73 Commits

Author SHA1 Message Date
9d9ab20952 Merge pull request 'Deploy nudges and action confirmation' (#311) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #311
2025-12-28 02:06:18 +00:00
e41d34dd3d Group buttons and add confirmation dialogues (#310)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Reviewer:
<img width="409" alt="image.png" src="attachments/a090c61e-a2d8-4685-ae64-547851d1ee84">
Submitter:
<img width="404" alt="image.png" src="attachments/9205a438-1f1f-4af4-b9a0-6a8d56580afa">
<img width="411" alt="image.png" src="attachments/7ae8115b-3376-4306-b9b9-acc12226abb3">
Admin:
<img width="392" alt="image.png" src="attachments/07a182d1-5375-4195-bfda-c14f09469cbe">
<img width="388" alt="image.png" src="attachments/ce82017d-5c1d-4a93-9247-9b5608f9030e">

Confirmation Dialogue:
<img width="545" alt="image.png" src="attachments/1efff8be-1d41-429e-8c6e-3d36b7dad128">

Example where both groups show up:
<img width="404" alt="image.png" src="attachments/b0ca4be2-7c58-4c0c-9a5f-dcd89e23b08f">

Reviewed-on: #310
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-12-28 00:34:58 +00:00
f49e27e230 Support editing map fix descriptions (#309)
All checks were successful
continuous-integration/drone/push Build is passing
The description can be edited by the **submitter** only if the status is Changes Requested or Under Construction.

<img width="734" alt="image.png" src="attachments/9fd7b838-f946-4091-a396-ef66f5e655bc">
<img width="724" alt="image.png" src="attachments/f65f059e-af97-448a-9627-fee827d30e59">

Reviewed-on: #309
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-12-27 23:40:42 +00:00
d500462fc7 Add user nudges for certain statuses (#308)
Some checks failed
continuous-integration/drone/push Build is failing
Will show a badge icon on the audit tab if there are any validator errors/checklists to direct attention to it. Will show nudge message ONLY to the submitter.

![image.png](/attachments/f5cd9ab6-b996-40b2-ad43-fa5e9b28caf5)
![image.png](/attachments/9aba2132-ec85-4ae9-b0fa-be253ecc2355)

Closes !205

Reviewed-on: #308
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-12-27 23:30:38 +00:00
ee2bc94312 Add releasing status to the processing list (#307)
All checks were successful
continuous-integration/drone/push Build is passing
Closes !269

Reviewed-on: #307
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-12-27 22:25:39 +00:00
67ece176c6 Merge pull request 'Deploy script review update' (#306) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #306
2025-12-27 21:49:28 +00:00
84edc71574 Add game name to review page (#305)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Deduped all the game name usage to a single lib. Closes !281

<img width="785" alt="image.png" src="attachments/0f226438-fed1-40b2-81a9-2988dd2d4a33">

Reviewed-on: #305
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-12-27 19:56:33 +00:00
7c5d8a2163 Add script review page (#304)
All checks were successful
continuous-integration/drone/push Build is passing
Closes !2

Added review dashboard button as well.

<img width="1313" alt="image.png" src="attachments/a2abd430-7ff6-431a-9261-82e026de58f5">

![image.png](/attachments/e1ba3536-2869-4661-b46c-007ddaff8f3e)

Reviewed-on: #304
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-12-27 19:56:19 +00:00
b31b3bed5f Merge pull request 'Deploy workflow timeline' (#302) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #302
2025-12-27 08:28:42 +00:00
7eaa84a0ed Change Timeline Text (#301)
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Some tweaks to the descriptions.  Evidently I didn't read carefully enough.

Reviewed-on: #301
Reviewed-by: itzaname <itzaname@noreply@itzana.me>
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2025-12-27 08:19:17 +00:00
cf0cf9da7a Add workflow timeline (#300)
All checks were successful
continuous-integration/drone/push Build is passing
Closes !232

<img width="763" alt="image.png" src="attachments/559715f5-630e-4029-a19b-c9f4cf4c7270">

Reviewed-on: #300
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-12-27 08:04:02 +00:00
34cd1c7c26 Merge pull request 'Deploy dashboard update' (#299) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #299
Reviewed-by: itzaname <itzaname@noreply@itzana.me>
2025-12-27 05:41:53 +00:00
74565e567a Fix "0" displaying in "Review Dashboard" button on user dashboard (#298)
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
The review dashboard link only shows when the user has the correct roles. A normal user would not see the button but instead the text "0".

Reviewed-on: #298
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-12-27 05:39:33 +00:00
ea65794255 Cycle before and after images every 1.5 seconds (#295)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
The images should auto cycle now that the thumbnails are working.

I don't know how to test this!  This is what I tried:
```
bun install
bun run build
VITE_API_HOST=https://maps.staging.strafes.net/v1 bun run preview
```
but the mapfixes page won't load the mapfixes.

Reviewed-on: #295
Reviewed-by: itzaname <itzaname@noreply@itzana.me>
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2025-12-27 05:26:04 +00:00
58706a5687 Add user/reviewer dashboard (#297)
All checks were successful
continuous-integration/drone/push Build is passing
Adds "at a glance" dashboard so life is less painful.

![image.png](/attachments/43e83777-7196-4274-9adc-e1268e43bc0f)
![image.png](/attachments/1cbe99ab-50b8-443a-aa48-ad9107ccfb1e)

Reviewed-on: #297
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-12-27 05:20:45 +00:00
efeb525e19 Merge pull request 'Add mapfix history on maps page' (#294) from feature/mapfix-list into staging
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #294
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
2025-12-27 04:51:03 +00:00
5a1fe60a7b fix quat docker
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-26 19:13:03 -08:00
01cfe67848 Just exclude rejected and released for active list
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-12-26 20:38:18 -05:00
a19bc4d380 Add mapfix history on maps page
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-26 20:32:55 -05:00
ae006565d6 Merge pull request 'Fix overflow on mapfix/submission' (#293) from fix/overflow into staging
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #293
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
2025-12-27 00:44:26 +00:00
57bca99109 Fix overflow
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-12-26 19:42:36 -05:00
058455efd2 Merge pull request 'Deploy updates' (#291) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #291
2025-12-26 04:46:53 +00:00
cd09c9b18e Populate username for map fixes by author id
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-12-25 20:42:22 -08:00
e48cbaff72 Make maps behave like normal link 2025-12-25 20:42:22 -08:00
140d58b808 Make comments support newlines 2025-12-25 20:42:22 -08:00
ba761549b8 Force dark theme 2025-12-25 20:42:22 -08:00
86643fef8d Merge branch 'master' into staging 2025-12-25 20:42:18 -08:00
96af864c5e Deploy staging to prod (#286)
All checks were successful
continuous-integration/drone/push Build is passing
Pull in validator changes and full ui rework to remove nextjs.

Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Reviewed-on: #286
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-12-26 03:30:36 +00:00
7db89fd99b Fix bun lock file
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-12-25 22:10:29 -05:00
f2bb1b078d Fix content width and standardize on skeleton loading
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2025-12-25 21:37:23 -05:00
66878fba4e Switch loading text to skeleton
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-25 21:02:15 -05:00
bda99550be Fix submission icon 2025-12-25 21:00:28 -05:00
8a216c7e82 Add username api
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-25 20:55:15 -05:00
e5277c05a1 Avatar image loading
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-25 20:38:17 -05:00
e4af76cfd4 Fix api endpoint
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-25 20:22:24 -05:00
30db1cc375 Fix the build issues
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-25 19:52:01 -05:00
b50c84f8cf Use port 3000
Some checks failed
continuous-integration/drone/push Build is failing
2025-12-25 19:49:52 -05:00
7589ef7df6 Fix dockerfile for spa
Some checks failed
continuous-integration/drone/push Build was killed
2025-12-25 19:49:06 -05:00
8ab8c441b0 Home page and header fixes
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-25 19:45:16 -05:00
a26b228ebe Add 404 page 2025-12-25 19:45:16 -05:00
3654755540 Thumbnail/nav cleanup 2025-12-25 19:45:16 -05:00
c2b50ffab2 Cleanup home/nav 2025-12-25 19:45:16 -05:00
75756917b1 some theming 2025-12-25 19:45:16 -05:00
8989c08857 theme 2025-12-25 19:45:16 -05:00
b2232f4177 Initial work to nuke nextjs 2025-12-25 19:45:16 -05:00
7d1c4d2b6c Add stats endpoint
Some checks failed
continuous-integration/drone Build was killed
continuous-integration/drone/push Build is passing
2025-12-25 18:58:52 -05:00
ca401d4b96 Add batch thumbnail endpoint (#285)
All checks were successful
continuous-integration/drone/push Build is passing
Step 1 of eliminating nextjs is adding a way to query thumbnails from roblox since nextjs handles that. This implements a batch endpoint and caching to do that. Bonus: thumbnails will actually work once we start using this.

Reviewed-on: #285
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-12-25 22:56:59 +00:00
9ab80931bf remove unfulfilled lints
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-09 14:34:39 -08:00
09022e7292 change allow to expect 2025-12-09 14:34:16 -08:00
47c0fff0ec Merge pull request 'Update javascript' (#283) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #283
2025-12-06 04:48:21 +00:00
b7c28616ad Merge pull request 'submissions: Fix Maps.Update Date + Release Date Mixup' (#282) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #282
2025-09-29 02:14:51 +00:00
89ab25dfb9 Merge pull request 'deploy fixes' (#279) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #279
2025-09-23 22:41:35 +00:00
b0b5ff0725 Merge pull request 'web: add missing button lost in refactor' (#275) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #275
2025-09-17 00:12:15 +00:00
0532965d37 Merge pull request 'Maps Metadata Maintenance' (#267) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #267
2025-08-16 06:24:19 +00:00
f59979987f Merge pull request 'Deploy Public API' (#256) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #256
2025-08-08 22:07:37 +00:00
a232269d54 Merge pull request 'Extend Web API Maps With New Fields' (#250) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #250
2025-07-26 03:29:09 +00:00
a7c4ca4b49 Merge pull request 'Implement Maps' (#248) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #248
2025-07-26 01:26:26 +00:00
ca9f82a5aa Merge pull request 'Set Download File Name' (#245) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #245
2025-07-23 09:32:27 +00:00
e1a2f6f075 Merge pull request 'Fix gRPC' (#244) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #244
2025-07-23 04:53:29 +00:00
dad904cd86 Merge pull request 'Convert Validator API to gRPC' (#239) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #239
2025-07-22 04:32:04 +00:00
ad7117a69c Merge pull request 'Scream Test Backend Overhaul' (#237) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #237
2025-07-18 06:28:18 +00:00
d566591ea6 Merge pull request 'Fix Audit Event Order + Check Unanchored Parts' (#234) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #234
2025-07-16 06:47:29 +00:00
424ef6238b Merge pull request 'Prevent Mapfix Duplicates + Correctly Report Transaction Errors' (#221) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #221
2025-07-01 12:40:26 +00:00
0f0ab4d3e0 Merge pull request 'Update Roblox Api + Update Deps' (#217) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #217
2025-07-01 08:47:27 +00:00
3e2d782289 Merge pull request 'QoL Web Changes + Map Download Permission Fix' (#214) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #214
2025-06-30 10:20:03 +00:00
dc446c545f Fix Bypass Submit + Audit Checklist + Map Download Button (#207)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #207
2025-06-24 06:41:56 +00:00
e234a87d05 Replace Bypass Submit With Submit Unchecked + Error Endpoint (#200)
Some checks are pending
continuous-integration/drone/push Build is running
Reviewed-on: #200
Co-authored-by: Quaternions <krakow20@gmail.com>
Co-committed-by: Quaternions <krakow20@gmail.com>
2025-06-23 23:39:18 -07:00
8ab772ea81 Validate Asset Version + Website QoL + Script Names Fix (#193)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #193
2025-06-10 23:53:07 +00:00
9b58b1d26a Frontend Rework (#185)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #185
2025-06-09 01:09:17 +00:00
7689001e74 Merge pull request '404 / 500 Thumbnails + Fix Regex Capture Groups' (#168) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #168
2025-06-07 04:02:26 +00:00
e89abed3d5 Merge pull request 'Thumbnail Fixes + Bypass Submit Button' (#161) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #161
2025-06-05 01:34:35 +00:00
b792d33164 Merge pull request 'Update Rust Dependencies (Roblox Format Zstd Support)' (#142) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #142
2025-06-01 23:13:58 +00:00
929b5949f0 Merge pull request 'Snapshot "Working" Code' (#139) from staging into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #139
2025-04-27 21:21:05 +00:00
116 changed files with 19754 additions and 2428 deletions

View File

@@ -34,7 +34,7 @@ services:
"--data-rpc-host","dataservice:9000", "--data-rpc-host","dataservice:9000",
] ]
env_file: env_file:
- ~/auth-compose/strafesnet_staging.env - /home/quat/auth-compose/strafesnet_staging.env
depends_on: depends_on:
- authrpc - authrpc
- nats - nats
@@ -59,7 +59,7 @@ services:
maptest-validator maptest-validator
container_name: validation container_name: validation
env_file: env_file:
- ~/auth-compose/strafesnet_staging.env - /home/quat/auth-compose/strafesnet_staging.env
environment: environment:
- ROBLOX_GROUP_ID=17032139 # "None" is special case string value - ROBLOX_GROUP_ID=17032139 # "None" is special case string value
- API_HOST_INTERNAL=http://submissions:8083/v1 - API_HOST_INTERNAL=http://submissions:8083/v1
@@ -105,7 +105,7 @@ services:
- REDIS_ADDR=authredis:6379 - REDIS_ADDR=authredis:6379
- RBX_GROUP_ID=17032139 - RBX_GROUP_ID=17032139
env_file: env_file:
- ~/auth-compose/auth-service.env - /home/quat/auth-compose/auth-service.env
depends_on: depends_on:
- authredis - authredis
networks: networks:
@@ -119,7 +119,7 @@ services:
environment: environment:
- REDIS_ADDR=authredis:6379 - REDIS_ADDR=authredis:6379
env_file: env_file:
- ~/auth-compose/auth-service.env - /home/quat/auth-compose/auth-service.env
depends_on: depends_on:
- authredis - authredis
networks: networks:

45
go.mod
View File

@@ -11,17 +11,18 @@ require (
github.com/dchest/siphash v1.2.3 github.com/dchest/siphash v1.2.3
github.com/gin-gonic/gin v1.10.1 github.com/gin-gonic/gin v1.10.1
github.com/go-faster/errors v0.7.1 github.com/go-faster/errors v0.7.1
github.com/go-faster/jx v1.1.0 github.com/go-faster/jx v1.2.0
github.com/nats-io/nats.go v1.37.0 github.com/nats-io/nats.go v1.37.0
github.com/ogen-go/ogen v1.2.1 github.com/ogen-go/ogen v1.18.0
github.com/redis/go-redis/v9 v9.10.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/swaggo/files v1.0.1 github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.6 github.com/swaggo/swag v1.16.6
github.com/urfave/cli/v2 v2.27.6 github.com/urfave/cli/v2 v2.27.6
go.opentelemetry.io/otel v1.32.0 go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/metric v1.32.0 go.opentelemetry.io/otel/metric v1.39.0
go.opentelemetry.io/otel/trace v1.32.0 go.opentelemetry.io/otel/trace v1.39.0
google.golang.org/grpc v1.48.0 google.golang.org/grpc v1.48.0
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.25.12 gorm.io/gorm v1.25.12
@@ -33,9 +34,11 @@ require (
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect
@@ -55,7 +58,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.6 // indirect github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect github.com/mailru/easyjson v0.7.6 // indirect
@@ -65,36 +68,38 @@ require (
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.17.0 // indirect golang.org/x/mod v0.31.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.40.0 // indirect
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
google.golang.org/protobuf v1.34.1 // indirect google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
require ( require (
github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/fatih/color v1.17.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-faster/yaml v0.4.6 // indirect github.com/go-faster/yaml v0.4.6 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
// github.com/golang/protobuf v1.5.4 // indirect // github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/asm v1.2.1 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.1 // indirect
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.12.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.32.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )

69
go.sum
View File

@@ -14,12 +14,18 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
@@ -39,8 +45,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -49,6 +59,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@@ -63,11 +75,13 @@ github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AY
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg= github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg=
github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg= github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI=
github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE=
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I= github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk= github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@@ -113,8 +127,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -140,6 +154,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@@ -159,6 +175,8 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -176,11 +194,15 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/ogen-go/ogen v1.2.1 h1:C5A0lvUMu2wl+eWIxnpXMWnuOJ26a2FyzR1CIC2qG0M= github.com/ogen-go/ogen v1.2.1 h1:C5A0lvUMu2wl+eWIxnpXMWnuOJ26a2FyzR1CIC2qG0M=
github.com/ogen-go/ogen v1.2.1/go.mod h1:P2zQdEu8UqaVRfD5GEFvl+9q63VjMLvDquq1wVbyInM= github.com/ogen-go/ogen v1.2.1/go.mod h1:P2zQdEu8UqaVRfD5GEFvl+9q63VjMLvDquq1wVbyInM=
github.com/ogen-go/ogen v1.18.0 h1:6RQ7lFBjOeNaUWu4getfqIh4GJbEY4hqKuzDtec/g60=
github.com/ogen-go/ogen v1.18.0/go.mod h1:dHFr2Wf6cA7tSxMI+zPC21UR5hAlDw8ZYUkK3PziURY=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
@@ -188,6 +210,10 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
@@ -204,8 +230,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
@@ -221,12 +248,14 @@ github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@@ -234,6 +263,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
@@ -242,15 +273,21 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg= golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -266,6 +303,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -275,6 +314,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -293,6 +334,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -303,6 +346,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -312,6 +357,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -14,15 +14,41 @@ tags:
description: Long-running operations description: Long-running operations
- name: Session - name: Session
description: Session queries description: Session queries
- name: Stats
description: Statistics queries
- name: Submissions - name: Submissions
description: Submission operations description: Submission operations
- name: Scripts - name: Scripts
description: Script operations description: Script operations
- name: ScriptPolicy - name: ScriptPolicy
description: Script policy operations description: Script policy operations
- name: Thumbnails
description: Thumbnail operations
- name: Users
description: User operations
security: security:
- cookieAuth: [] - cookieAuth: []
paths: paths:
/stats:
get:
summary: Get aggregate statistics
operationId: getStats
tags:
- Stats
security: []
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: "#/components/schemas/Stats"
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/session/user: /session/user:
get: get:
summary: Get information about the currently logged in user summary: Get information about the currently logged in user
@@ -421,6 +447,30 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/Error" $ref: "#/components/schemas/Error"
/mapfixes/{MapfixID}/description:
patch:
summary: Update description (submitter only)
operationId: updateMapfixDescription
tags:
- Mapfixes
parameters:
- $ref: '#/components/parameters/MapfixID'
requestBody:
required: true
content:
text/plain:
schema:
type: string
maxLength: 256
responses:
"204":
description: Successful response
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/mapfixes/{MapfixID}/completed: /mapfixes/{MapfixID}/completed:
post: post:
summary: Called by maptest when a player completes the map summary: Called by maptest when a player completes the map
@@ -1438,6 +1488,222 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/Error" $ref: "#/components/schemas/Error"
/thumbnails/assets:
post:
summary: Batch fetch asset thumbnails
operationId: batchAssetThumbnails
tags:
- Thumbnails
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- assetIds
properties:
assetIds:
type: array
items:
type: integer
format: uint64
maxItems: 100
description: Array of asset IDs (max 100)
size:
type: string
enum:
- "150x150"
- "420x420"
- "768x432"
default: "420x420"
description: Thumbnail size
responses:
"200":
description: Successful response
content:
application/json:
schema:
type: object
properties:
thumbnails:
type: object
additionalProperties:
type: string
description: Map of asset ID to thumbnail URL
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/thumbnails/asset/{AssetID}:
get:
summary: Get single asset thumbnail
operationId: getAssetThumbnail
tags:
- Thumbnails
security: []
parameters:
- name: AssetID
in: path
required: true
schema:
type: integer
format: uint64
- name: size
in: query
schema:
type: string
enum:
- "150x150"
- "420x420"
- "768x432"
default: "420x420"
responses:
"302":
description: Redirect to thumbnail URL
headers:
Location:
description: URL to redirect to
schema:
type: string
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/thumbnails/users:
post:
summary: Batch fetch user avatar thumbnails
operationId: batchUserThumbnails
tags:
- Thumbnails
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- userIds
properties:
userIds:
type: array
items:
type: integer
format: uint64
maxItems: 100
description: Array of user IDs (max 100)
size:
type: string
enum:
- "150x150"
- "420x420"
- "768x432"
default: "150x150"
description: Thumbnail size
responses:
"200":
description: Successful response
content:
application/json:
schema:
type: object
properties:
thumbnails:
type: object
additionalProperties:
type: string
description: Map of user ID to thumbnail URL
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/thumbnails/user/{UserID}:
get:
summary: Get single user avatar thumbnail
operationId: getUserThumbnail
tags:
- Thumbnails
security: []
parameters:
- name: UserID
in: path
required: true
schema:
type: integer
format: uint64
- name: size
in: query
schema:
type: string
enum:
- "150x150"
- "420x420"
- "768x432"
default: "150x150"
responses:
"302":
description: Redirect to thumbnail URL
headers:
Location:
description: URL to redirect to
schema:
type: string
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/usernames:
post:
summary: Batch fetch usernames
operationId: batchUsernames
tags:
- Users
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- userIds
properties:
userIds:
type: array
items:
type: integer
format: uint64
maxItems: 100
description: Array of user IDs (max 100)
responses:
"200":
description: Successful response
content:
application/json:
schema:
type: object
properties:
usernames:
type: object
additionalProperties:
type: string
description: Map of user ID to username
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components: components:
securitySchemes: securitySchemes:
cookieAuth: cookieAuth:
@@ -2061,6 +2327,47 @@ components:
type: integer type: integer
format: int32 format: int32
minimum: 0 minimum: 0
Stats:
description: Aggregate statistics for submissions and mapfixes
type: object
properties:
TotalSubmissions:
type: integer
format: int64
minimum: 0
description: Total number of submissions
TotalMapfixes:
type: integer
format: int64
minimum: 0
description: Total number of mapfixes
ReleasedSubmissions:
type: integer
format: int64
minimum: 0
description: Number of released submissions
ReleasedMapfixes:
type: integer
format: int64
minimum: 0
description: Number of released mapfixes
SubmittedSubmissions:
type: integer
format: int64
minimum: 0
description: Number of submissions under review
SubmittedMapfixes:
type: integer
format: int64
minimum: 0
description: Number of mapfixes under review
required:
- TotalSubmissions
- TotalMapfixes
- ReleasedSubmissions
- ReleasedMapfixes
- SubmittedSubmissions
- SubmittedMapfixes
Error: Error:
description: Represents error object description: Represents error object
type: object type: object

View File

@@ -5,14 +5,14 @@ package api
import ( import (
"net/http" "net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
ht "github.com/ogen-go/ogen/http" ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/middleware" "github.com/ogen-go/ogen/middleware"
"github.com/ogen-go/ogen/ogenerrors" "github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/otelogen" "github.com/ogen-go/ogen/otelogen"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
) )
var ( var (
@@ -32,6 +32,7 @@ type otelConfig struct {
Tracer trace.Tracer Tracer trace.Tracer
MeterProvider metric.MeterProvider MeterProvider metric.MeterProvider
Meter metric.Meter Meter metric.Meter
Attributes []attribute.KeyValue
} }
func (cfg *otelConfig) initOTEL() { func (cfg *otelConfig) initOTEL() {
@@ -215,6 +216,13 @@ func WithMeterProvider(provider metric.MeterProvider) Option {
}) })
} }
// WithAttributes specifies default otel attributes.
func WithAttributes(attributes ...attribute.KeyValue) Option {
return otelOptionFunc(func(cfg *otelConfig) {
cfg.Attributes = attributes
})
}
// WithClient specifies http client to use. // WithClient specifies http client to use.
func WithClient(client ht.Client) ClientOption { func WithClient(client ht.Client) ClientOption {
return optionFunc[clientConfig](func(cfg *clientConfig) { return optionFunc[clientConfig](func(cfg *clientConfig) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
// Code generated by ogen, DO NOT EDIT.
package api
// setDefaults set default value of fields.
func (s *BatchAssetThumbnailsReq) setDefaults() {
{
val := BatchAssetThumbnailsReqSize("420x420")
s.Size.SetTo(val)
}
}
// setDefaults set default value of fields.
func (s *BatchUserThumbnailsReq) setDefaults() {
{
val := BatchUserThumbnailsReqSize("150x150")
s.Size.SetTo(val)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,9 @@ const (
ActionSubmissionTriggerUploadOperation OperationName = "ActionSubmissionTriggerUpload" ActionSubmissionTriggerUploadOperation OperationName = "ActionSubmissionTriggerUpload"
ActionSubmissionTriggerValidateOperation OperationName = "ActionSubmissionTriggerValidate" ActionSubmissionTriggerValidateOperation OperationName = "ActionSubmissionTriggerValidate"
ActionSubmissionValidatedOperation OperationName = "ActionSubmissionValidated" ActionSubmissionValidatedOperation OperationName = "ActionSubmissionValidated"
BatchAssetThumbnailsOperation OperationName = "BatchAssetThumbnails"
BatchUserThumbnailsOperation OperationName = "BatchUserThumbnails"
BatchUsernamesOperation OperationName = "BatchUsernames"
CreateMapfixOperation OperationName = "CreateMapfix" CreateMapfixOperation OperationName = "CreateMapfix"
CreateMapfixAuditCommentOperation OperationName = "CreateMapfixAuditComment" CreateMapfixAuditCommentOperation OperationName = "CreateMapfixAuditComment"
CreateScriptOperation OperationName = "CreateScript" CreateScriptOperation OperationName = "CreateScript"
@@ -40,12 +43,15 @@ const (
DeleteScriptOperation OperationName = "DeleteScript" DeleteScriptOperation OperationName = "DeleteScript"
DeleteScriptPolicyOperation OperationName = "DeleteScriptPolicy" DeleteScriptPolicyOperation OperationName = "DeleteScriptPolicy"
DownloadMapAssetOperation OperationName = "DownloadMapAsset" DownloadMapAssetOperation OperationName = "DownloadMapAsset"
GetAssetThumbnailOperation OperationName = "GetAssetThumbnail"
GetMapOperation OperationName = "GetMap" GetMapOperation OperationName = "GetMap"
GetMapfixOperation OperationName = "GetMapfix" GetMapfixOperation OperationName = "GetMapfix"
GetOperationOperation OperationName = "GetOperation" GetOperationOperation OperationName = "GetOperation"
GetScriptOperation OperationName = "GetScript" GetScriptOperation OperationName = "GetScript"
GetScriptPolicyOperation OperationName = "GetScriptPolicy" GetScriptPolicyOperation OperationName = "GetScriptPolicy"
GetStatsOperation OperationName = "GetStats"
GetSubmissionOperation OperationName = "GetSubmission" GetSubmissionOperation OperationName = "GetSubmission"
GetUserThumbnailOperation OperationName = "GetUserThumbnail"
ListMapfixAuditEventsOperation OperationName = "ListMapfixAuditEvents" ListMapfixAuditEventsOperation OperationName = "ListMapfixAuditEvents"
ListMapfixesOperation OperationName = "ListMapfixes" ListMapfixesOperation OperationName = "ListMapfixes"
ListMapsOperation OperationName = "ListMaps" ListMapsOperation OperationName = "ListMaps"
@@ -59,6 +65,7 @@ const (
SessionValidateOperation OperationName = "SessionValidate" SessionValidateOperation OperationName = "SessionValidate"
SetMapfixCompletedOperation OperationName = "SetMapfixCompleted" SetMapfixCompletedOperation OperationName = "SetMapfixCompleted"
SetSubmissionCompletedOperation OperationName = "SetSubmissionCompleted" SetSubmissionCompletedOperation OperationName = "SetSubmissionCompleted"
UpdateMapfixDescriptionOperation OperationName = "UpdateMapfixDescription"
UpdateMapfixModelOperation OperationName = "UpdateMapfixModel" UpdateMapfixModelOperation OperationName = "UpdateMapfixModel"
UpdateScriptOperation OperationName = "UpdateScript" UpdateScriptOperation OperationName = "UpdateScript"
UpdateScriptPolicyOperation OperationName = "UpdateScriptPolicy" UpdateScriptPolicyOperation OperationName = "UpdateScriptPolicy"

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
package api package api
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"mime" "mime"
@@ -10,13 +11,13 @@ import (
"github.com/go-faster/errors" "github.com/go-faster/errors"
"github.com/go-faster/jx" "github.com/go-faster/jx"
"github.com/ogen-go/ogen/ogenerrors" "github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/validate" "github.com/ogen-go/ogen/validate"
) )
func (s *Server) decodeCreateMapfixRequest(r *http.Request) ( func (s *Server) decodeBatchAssetThumbnailsRequest(r *http.Request) (
req *MapfixTriggerCreate, req *BatchAssetThumbnailsReq,
rawBody []byte,
close func() error, close func() error,
rerr error, rerr error,
) { ) {
@@ -37,22 +38,266 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
}() }()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return req, close, errors.Wrap(err, "parse media type") return req, rawBody, close, errors.Wrap(err, "parse media type")
} }
switch { switch {
case ct == "application/json": case ct == "application/json":
if r.ContentLength == 0 { if r.ContentLength == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
buf, err := io.ReadAll(r.Body) buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil { if err != nil {
return req, close, err return req, rawBody, close, err
} }
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 { if len(buf) == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request BatchAssetThumbnailsReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeBatchUserThumbnailsRequest(r *http.Request) (
req *BatchUserThumbnailsReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request BatchUserThumbnailsReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeBatchUsernamesRequest(r *http.Request) (
req *BatchUsernamesReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request BatchUsernamesReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
req *MapfixTriggerCreate,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf) d := jx.DecodeBytes(buf)
var request MapfixTriggerCreate var request MapfixTriggerCreate
@@ -70,7 +315,7 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
Body: buf, Body: buf,
Err: err, Err: err,
} }
return req, close, err return req, rawBody, close, err
} }
if err := func() error { if err := func() error {
if err := request.Validate(); err != nil { if err := request.Validate(); err != nil {
@@ -78,16 +323,17 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
} }
return nil return nil
}(); err != nil { }(); err != nil {
return req, close, errors.Wrap(err, "validate") return req, rawBody, close, errors.Wrap(err, "validate")
} }
return &request, close, nil return &request, rawBody, close, nil
default: default:
return req, close, validate.InvalidContentType(ct) return req, rawBody, close, validate.InvalidContentType(ct)
} }
} }
func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) ( func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
req CreateMapfixAuditCommentReq, req CreateMapfixAuditCommentReq,
rawBody []byte,
close func() error, close func() error,
rerr error, rerr error,
) { ) {
@@ -108,20 +354,21 @@ func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
}() }()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return req, close, errors.Wrap(err, "parse media type") return req, rawBody, close, errors.Wrap(err, "parse media type")
} }
switch { switch {
case ct == "text/plain": case ct == "text/plain":
reader := r.Body reader := r.Body
request := CreateMapfixAuditCommentReq{Data: reader} request := CreateMapfixAuditCommentReq{Data: reader}
return request, close, nil return request, rawBody, close, nil
default: default:
return req, close, validate.InvalidContentType(ct) return req, rawBody, close, validate.InvalidContentType(ct)
} }
} }
func (s *Server) decodeCreateScriptRequest(r *http.Request) ( func (s *Server) decodeCreateScriptRequest(r *http.Request) (
req *ScriptCreate, req *ScriptCreate,
rawBody []byte,
close func() error, close func() error,
rerr error, rerr error,
) { ) {
@@ -142,22 +389,29 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
}() }()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return req, close, errors.Wrap(err, "parse media type") return req, rawBody, close, errors.Wrap(err, "parse media type")
} }
switch { switch {
case ct == "application/json": case ct == "application/json":
if r.ContentLength == 0 { if r.ContentLength == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
buf, err := io.ReadAll(r.Body) buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil { if err != nil {
return req, close, err return req, rawBody, close, err
} }
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 { if len(buf) == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf) d := jx.DecodeBytes(buf)
var request ScriptCreate var request ScriptCreate
@@ -175,7 +429,7 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
Body: buf, Body: buf,
Err: err, Err: err,
} }
return req, close, err return req, rawBody, close, err
} }
if err := func() error { if err := func() error {
if err := request.Validate(); err != nil { if err := request.Validate(); err != nil {
@@ -183,16 +437,17 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
} }
return nil return nil
}(); err != nil { }(); err != nil {
return req, close, errors.Wrap(err, "validate") return req, rawBody, close, errors.Wrap(err, "validate")
} }
return &request, close, nil return &request, rawBody, close, nil
default: default:
return req, close, validate.InvalidContentType(ct) return req, rawBody, close, validate.InvalidContentType(ct)
} }
} }
func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) ( func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
req *ScriptPolicyCreate, req *ScriptPolicyCreate,
rawBody []byte,
close func() error, close func() error,
rerr error, rerr error,
) { ) {
@@ -213,22 +468,29 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
}() }()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return req, close, errors.Wrap(err, "parse media type") return req, rawBody, close, errors.Wrap(err, "parse media type")
} }
switch { switch {
case ct == "application/json": case ct == "application/json":
if r.ContentLength == 0 { if r.ContentLength == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
buf, err := io.ReadAll(r.Body) buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil { if err != nil {
return req, close, err return req, rawBody, close, err
} }
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 { if len(buf) == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf) d := jx.DecodeBytes(buf)
var request ScriptPolicyCreate var request ScriptPolicyCreate
@@ -246,7 +508,7 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
Body: buf, Body: buf,
Err: err, Err: err,
} }
return req, close, err return req, rawBody, close, err
} }
if err := func() error { if err := func() error {
if err := request.Validate(); err != nil { if err := request.Validate(); err != nil {
@@ -254,16 +516,17 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
} }
return nil return nil
}(); err != nil { }(); err != nil {
return req, close, errors.Wrap(err, "validate") return req, rawBody, close, errors.Wrap(err, "validate")
} }
return &request, close, nil return &request, rawBody, close, nil
default: default:
return req, close, validate.InvalidContentType(ct) return req, rawBody, close, validate.InvalidContentType(ct)
} }
} }
func (s *Server) decodeCreateSubmissionRequest(r *http.Request) ( func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
req *SubmissionTriggerCreate, req *SubmissionTriggerCreate,
rawBody []byte,
close func() error, close func() error,
rerr error, rerr error,
) { ) {
@@ -284,22 +547,29 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
}() }()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return req, close, errors.Wrap(err, "parse media type") return req, rawBody, close, errors.Wrap(err, "parse media type")
} }
switch { switch {
case ct == "application/json": case ct == "application/json":
if r.ContentLength == 0 { if r.ContentLength == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
buf, err := io.ReadAll(r.Body) buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil { if err != nil {
return req, close, err return req, rawBody, close, err
} }
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 { if len(buf) == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf) d := jx.DecodeBytes(buf)
var request SubmissionTriggerCreate var request SubmissionTriggerCreate
@@ -317,7 +587,7 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
Body: buf, Body: buf,
Err: err, Err: err,
} }
return req, close, err return req, rawBody, close, err
} }
if err := func() error { if err := func() error {
if err := request.Validate(); err != nil { if err := request.Validate(); err != nil {
@@ -325,16 +595,17 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
} }
return nil return nil
}(); err != nil { }(); err != nil {
return req, close, errors.Wrap(err, "validate") return req, rawBody, close, errors.Wrap(err, "validate")
} }
return &request, close, nil return &request, rawBody, close, nil
default: default:
return req, close, validate.InvalidContentType(ct) return req, rawBody, close, validate.InvalidContentType(ct)
} }
} }
func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) ( func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
req *SubmissionTriggerCreate, req *SubmissionTriggerCreate,
rawBody []byte,
close func() error, close func() error,
rerr error, rerr error,
) { ) {
@@ -355,22 +626,29 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
}() }()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return req, close, errors.Wrap(err, "parse media type") return req, rawBody, close, errors.Wrap(err, "parse media type")
} }
switch { switch {
case ct == "application/json": case ct == "application/json":
if r.ContentLength == 0 { if r.ContentLength == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
buf, err := io.ReadAll(r.Body) buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil { if err != nil {
return req, close, err return req, rawBody, close, err
} }
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 { if len(buf) == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf) d := jx.DecodeBytes(buf)
var request SubmissionTriggerCreate var request SubmissionTriggerCreate
@@ -388,7 +666,7 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
Body: buf, Body: buf,
Err: err, Err: err,
} }
return req, close, err return req, rawBody, close, err
} }
if err := func() error { if err := func() error {
if err := request.Validate(); err != nil { if err := request.Validate(); err != nil {
@@ -396,16 +674,17 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
} }
return nil return nil
}(); err != nil { }(); err != nil {
return req, close, errors.Wrap(err, "validate") return req, rawBody, close, errors.Wrap(err, "validate")
} }
return &request, close, nil return &request, rawBody, close, nil
default: default:
return req, close, validate.InvalidContentType(ct) return req, rawBody, close, validate.InvalidContentType(ct)
} }
} }
func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) ( func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
req CreateSubmissionAuditCommentReq, req CreateSubmissionAuditCommentReq,
rawBody []byte,
close func() error, close func() error,
rerr error, rerr error,
) { ) {
@@ -426,20 +705,21 @@ func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
}() }()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return req, close, errors.Wrap(err, "parse media type") return req, rawBody, close, errors.Wrap(err, "parse media type")
} }
switch { switch {
case ct == "text/plain": case ct == "text/plain":
reader := r.Body reader := r.Body
request := CreateSubmissionAuditCommentReq{Data: reader} request := CreateSubmissionAuditCommentReq{Data: reader}
return request, close, nil return request, rawBody, close, nil
default: default:
return req, close, validate.InvalidContentType(ct) return req, rawBody, close, validate.InvalidContentType(ct)
} }
} }
func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) ( func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
req []ReleaseInfo, req []ReleaseInfo,
rawBody []byte,
close func() error, close func() error,
rerr error, rerr error,
) { ) {
@@ -460,22 +740,29 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
}() }()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return req, close, errors.Wrap(err, "parse media type") return req, rawBody, close, errors.Wrap(err, "parse media type")
} }
switch { switch {
case ct == "application/json": case ct == "application/json":
if r.ContentLength == 0 { if r.ContentLength == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
buf, err := io.ReadAll(r.Body) buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil { if err != nil {
return req, close, err return req, rawBody, close, err
} }
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 { if len(buf) == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf) d := jx.DecodeBytes(buf)
var request []ReleaseInfo var request []ReleaseInfo
@@ -501,7 +788,7 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
Body: buf, Body: buf,
Err: err, Err: err,
} }
return req, close, err return req, rawBody, close, err
} }
if err := func() error { if err := func() error {
if request == nil { if request == nil {
@@ -534,16 +821,17 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
} }
return nil return nil
}(); err != nil { }(); err != nil {
return req, close, errors.Wrap(err, "validate") return req, rawBody, close, errors.Wrap(err, "validate")
} }
return request, close, nil return request, rawBody, close, nil
default: default:
return req, close, validate.InvalidContentType(ct) return req, rawBody, close, validate.InvalidContentType(ct)
} }
} }
func (s *Server) decodeUpdateScriptRequest(r *http.Request) ( func (s *Server) decodeUpdateMapfixDescriptionRequest(r *http.Request) (
req *ScriptUpdate, req UpdateMapfixDescriptionReq,
rawBody []byte,
close func() error, close func() error,
rerr error, rerr error,
) { ) {
@@ -564,22 +852,64 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
}() }()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return req, close, errors.Wrap(err, "parse media type") return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "text/plain":
reader := r.Body
request := UpdateMapfixDescriptionReq{Data: reader}
return request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
req *ScriptUpdate,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
} }
switch { switch {
case ct == "application/json": case ct == "application/json":
if r.ContentLength == 0 { if r.ContentLength == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
buf, err := io.ReadAll(r.Body) buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil { if err != nil {
return req, close, err return req, rawBody, close, err
} }
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 { if len(buf) == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf) d := jx.DecodeBytes(buf)
var request ScriptUpdate var request ScriptUpdate
@@ -597,7 +927,7 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
Body: buf, Body: buf,
Err: err, Err: err,
} }
return req, close, err return req, rawBody, close, err
} }
if err := func() error { if err := func() error {
if err := request.Validate(); err != nil { if err := request.Validate(); err != nil {
@@ -605,16 +935,17 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
} }
return nil return nil
}(); err != nil { }(); err != nil {
return req, close, errors.Wrap(err, "validate") return req, rawBody, close, errors.Wrap(err, "validate")
} }
return &request, close, nil return &request, rawBody, close, nil
default: default:
return req, close, validate.InvalidContentType(ct) return req, rawBody, close, validate.InvalidContentType(ct)
} }
} }
func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) ( func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
req *ScriptPolicyUpdate, req *ScriptPolicyUpdate,
rawBody []byte,
close func() error, close func() error,
rerr error, rerr error,
) { ) {
@@ -635,22 +966,29 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
}() }()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return req, close, errors.Wrap(err, "parse media type") return req, rawBody, close, errors.Wrap(err, "parse media type")
} }
switch { switch {
case ct == "application/json": case ct == "application/json":
if r.ContentLength == 0 { if r.ContentLength == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
buf, err := io.ReadAll(r.Body) buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil { if err != nil {
return req, close, err return req, rawBody, close, err
} }
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 { if len(buf) == 0 {
return req, close, validate.ErrBodyRequired return req, rawBody, close, validate.ErrBodyRequired
} }
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf) d := jx.DecodeBytes(buf)
var request ScriptPolicyUpdate var request ScriptPolicyUpdate
@@ -668,7 +1006,7 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
Body: buf, Body: buf,
Err: err, Err: err,
} }
return req, close, err return req, rawBody, close, err
} }
if err := func() error { if err := func() error {
if err := request.Validate(); err != nil { if err := request.Validate(); err != nil {
@@ -676,10 +1014,10 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
} }
return nil return nil
}(); err != nil { }(); err != nil {
return req, close, errors.Wrap(err, "validate") return req, rawBody, close, errors.Wrap(err, "validate")
} }
return &request, close, nil return &request, rawBody, close, nil
default: default:
return req, close, validate.InvalidContentType(ct) return req, rawBody, close, validate.InvalidContentType(ct)
} }
} }

View File

@@ -7,10 +7,51 @@ import (
"net/http" "net/http"
"github.com/go-faster/jx" "github.com/go-faster/jx"
ht "github.com/ogen-go/ogen/http" ht "github.com/ogen-go/ogen/http"
) )
func encodeBatchAssetThumbnailsRequest(
req *BatchAssetThumbnailsReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeBatchUserThumbnailsRequest(
req *BatchUserThumbnailsReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeBatchUsernamesRequest(
req *BatchUsernamesReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeCreateMapfixRequest( func encodeCreateMapfixRequest(
req *MapfixTriggerCreate, req *MapfixTriggerCreate,
r *http.Request, r *http.Request,
@@ -119,6 +160,16 @@ func encodeReleaseSubmissionsRequest(
return nil return nil
} }
func encodeUpdateMapfixDescriptionRequest(
req UpdateMapfixDescriptionReq,
r *http.Request,
) error {
const contentType = "text/plain"
body := req
ht.SetBody(r, body, contentType)
return nil
}
func encodeUpdateScriptRequest( func encodeUpdateScriptRequest(
req *ScriptUpdate, req *ScriptUpdate,
r *http.Request, r *http.Request,

View File

@@ -11,8 +11,9 @@ import (
"github.com/go-faster/errors" "github.com/go-faster/errors"
"github.com/go-faster/jx" "github.com/go-faster/jx"
"github.com/ogen-go/ogen/conv"
"github.com/ogen-go/ogen/ogenerrors" "github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/uri"
"github.com/ogen-go/ogen/validate" "github.com/ogen-go/ogen/validate"
) )
@@ -1456,6 +1457,282 @@ func decodeActionSubmissionValidatedResponse(resp *http.Response) (res *ActionSu
return res, errors.Wrap(defRes, "error") return res, errors.Wrap(defRes, "error")
} }
func decodeBatchAssetThumbnailsResponse(resp *http.Response) (res *BatchAssetThumbnailsOK, _ error) {
switch resp.StatusCode {
case 200:
// Code 200.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response BatchAssetThumbnailsOK
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeBatchUserThumbnailsResponse(resp *http.Response) (res *BatchUserThumbnailsOK, _ error) {
switch resp.StatusCode {
case 200:
// Code 200.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response BatchUserThumbnailsOK
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeBatchUsernamesResponse(resp *http.Response) (res *BatchUsernamesOK, _ error) {
switch resp.StatusCode {
case 200:
// Code 200.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response BatchUsernamesOK
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeCreateMapfixResponse(resp *http.Response) (res *OperationID, _ error) { func decodeCreateMapfixResponse(resp *http.Response) (res *OperationID, _ error) {
switch resp.StatusCode { switch resp.StatusCode {
case 201: case 201:
@@ -2277,6 +2554,105 @@ func decodeDownloadMapAssetResponse(resp *http.Response) (res DownloadMapAssetOK
return res, errors.Wrap(defRes, "error") return res, errors.Wrap(defRes, "error")
} }
func decodeGetAssetThumbnailResponse(resp *http.Response) (res *GetAssetThumbnailFound, _ error) {
switch resp.StatusCode {
case 302:
// Code 302.
var wrapper GetAssetThumbnailFound
h := uri.NewHeaderDecoder(resp.Header)
// Parse "Location" header.
{
cfg := uri.HeaderParameterDecodingConfig{
Name: "Location",
Explode: false,
}
if err := func() error {
if err := h.HasParam(cfg); err == nil {
if err := h.DecodeParam(cfg, func(d uri.Decoder) error {
var wrapperDotLocationVal string
if err := func() error {
val, err := d.DecodeValue()
if err != nil {
return err
}
c, err := conv.ToString(val)
if err != nil {
return err
}
wrapperDotLocationVal = c
return nil
}(); err != nil {
return err
}
wrapper.Location.SetTo(wrapperDotLocationVal)
return nil
}); err != nil {
return err
}
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "parse Location header")
}
}
return &wrapper, nil
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeGetMapResponse(resp *http.Response) (res *Map, _ error) { func decodeGetMapResponse(resp *http.Response) (res *Map, _ error) {
switch resp.StatusCode { switch resp.StatusCode {
case 200: case 200:
@@ -2782,6 +3158,107 @@ func decodeGetScriptPolicyResponse(resp *http.Response) (res *ScriptPolicy, _ er
return res, errors.Wrap(defRes, "error") return res, errors.Wrap(defRes, "error")
} }
func decodeGetStatsResponse(resp *http.Response) (res *Stats, _ error) {
switch resp.StatusCode {
case 200:
// Code 200.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Stats
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeGetSubmissionResponse(resp *http.Response) (res *Submission, _ error) { func decodeGetSubmissionResponse(resp *http.Response) (res *Submission, _ error) {
switch resp.StatusCode { switch resp.StatusCode {
case 200: case 200:
@@ -2883,6 +3360,105 @@ func decodeGetSubmissionResponse(resp *http.Response) (res *Submission, _ error)
return res, errors.Wrap(defRes, "error") return res, errors.Wrap(defRes, "error")
} }
func decodeGetUserThumbnailResponse(resp *http.Response) (res *GetUserThumbnailFound, _ error) {
switch resp.StatusCode {
case 302:
// Code 302.
var wrapper GetUserThumbnailFound
h := uri.NewHeaderDecoder(resp.Header)
// Parse "Location" header.
{
cfg := uri.HeaderParameterDecodingConfig{
Name: "Location",
Explode: false,
}
if err := func() error {
if err := h.HasParam(cfg); err == nil {
if err := h.DecodeParam(cfg, func(d uri.Decoder) error {
var wrapperDotLocationVal string
if err := func() error {
val, err := d.DecodeValue()
if err != nil {
return err
}
c, err := conv.ToString(val)
if err != nil {
return err
}
wrapperDotLocationVal = c
return nil
}(); err != nil {
return err
}
wrapper.Location.SetTo(wrapperDotLocationVal)
return nil
}); err != nil {
return err
}
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "parse Location header")
}
}
return &wrapper, nil
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeListMapfixAuditEventsResponse(resp *http.Response) (res []AuditEvent, _ error) { func decodeListMapfixAuditEventsResponse(resp *http.Response) (res []AuditEvent, _ error) {
switch resp.StatusCode { switch resp.StatusCode {
case 200: case 200:
@@ -4232,6 +4808,66 @@ func decodeSetSubmissionCompletedResponse(resp *http.Response) (res *SetSubmissi
return res, errors.Wrap(defRes, "error") return res, errors.Wrap(defRes, "error")
} }
func decodeUpdateMapfixDescriptionResponse(resp *http.Response) (res *UpdateMapfixDescriptionNoContent, _ error) {
switch resp.StatusCode {
case 204:
// Code 204.
return &UpdateMapfixDescriptionNoContent{}, nil
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeUpdateMapfixModelResponse(resp *http.Response) (res *UpdateMapfixModelNoContent, _ error) { func decodeUpdateMapfixModelResponse(resp *http.Response) (res *UpdateMapfixModelNoContent, _ error) {
switch resp.StatusCode { switch resp.StatusCode {
case 204: case 204:

View File

@@ -8,10 +8,11 @@ import (
"github.com/go-faster/errors" "github.com/go-faster/errors"
"github.com/go-faster/jx" "github.com/go-faster/jx"
"github.com/ogen-go/ogen/conv"
ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/uri"
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
ht "github.com/ogen-go/ogen/http"
) )
func encodeActionMapfixAcceptedResponse(response *ActionMapfixAcceptedNoContent, w http.ResponseWriter, span trace.Span) error { func encodeActionMapfixAcceptedResponse(response *ActionMapfixAcceptedNoContent, w http.ResponseWriter, span trace.Span) error {
@@ -182,6 +183,48 @@ func encodeActionSubmissionValidatedResponse(response *ActionSubmissionValidated
return nil return nil
} }
func encodeBatchAssetThumbnailsResponse(response *BatchAssetThumbnailsOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
span.SetStatus(codes.Ok, http.StatusText(200))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeBatchUserThumbnailsResponse(response *BatchUserThumbnailsOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
span.SetStatus(codes.Ok, http.StatusText(200))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeBatchUsernamesResponse(response *BatchUsernamesOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
span.SetStatus(codes.Ok, http.StatusText(200))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeCreateMapfixResponse(response *OperationID, w http.ResponseWriter, span trace.Span) error { func encodeCreateMapfixResponse(response *OperationID, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(201) w.WriteHeader(201)
@@ -296,6 +339,32 @@ func encodeDownloadMapAssetResponse(response DownloadMapAssetOK, w http.Response
return nil return nil
} }
func encodeGetAssetThumbnailResponse(response *GetAssetThumbnailFound, w http.ResponseWriter, span trace.Span) error {
// Encoding response headers.
{
h := uri.NewHeaderEncoder(w.Header())
// Encode "Location" header.
{
cfg := uri.HeaderParameterEncodingConfig{
Name: "Location",
Explode: false,
}
if err := h.EncodeParam(cfg, func(e uri.Encoder) error {
if val, ok := response.Location.Get(); ok {
return e.EncodeValue(conv.StringToString(val))
}
return nil
}); err != nil {
return errors.Wrap(err, "encode Location header")
}
}
}
w.WriteHeader(302)
span.SetStatus(codes.Ok, http.StatusText(302))
return nil
}
func encodeGetMapResponse(response *Map, w http.ResponseWriter, span trace.Span) error { func encodeGetMapResponse(response *Map, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200) w.WriteHeader(200)
@@ -366,6 +435,20 @@ func encodeGetScriptPolicyResponse(response *ScriptPolicy, w http.ResponseWriter
return nil return nil
} }
func encodeGetStatsResponse(response *Stats, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
span.SetStatus(codes.Ok, http.StatusText(200))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, span trace.Span) error { func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200) w.WriteHeader(200)
@@ -380,6 +463,32 @@ func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, sp
return nil return nil
} }
func encodeGetUserThumbnailResponse(response *GetUserThumbnailFound, w http.ResponseWriter, span trace.Span) error {
// Encoding response headers.
{
h := uri.NewHeaderEncoder(w.Header())
// Encode "Location" header.
{
cfg := uri.HeaderParameterEncodingConfig{
Name: "Location",
Explode: false,
}
if err := h.EncodeParam(cfg, func(e uri.Encoder) error {
if val, ok := response.Location.Get(); ok {
return e.EncodeValue(conv.StringToString(val))
}
return nil
}); err != nil {
return errors.Wrap(err, "encode Location header")
}
}
}
w.WriteHeader(302)
span.SetStatus(codes.Ok, http.StatusText(302))
return nil
}
func encodeListMapfixAuditEventsResponse(response []AuditEvent, w http.ResponseWriter, span trace.Span) error { func encodeListMapfixAuditEventsResponse(response []AuditEvent, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200) w.WriteHeader(200)
@@ -568,6 +677,13 @@ func encodeSetSubmissionCompletedResponse(response *SetSubmissionCompletedNoCont
return nil return nil
} }
func encodeUpdateMapfixDescriptionResponse(response *UpdateMapfixDescriptionNoContent, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(204)
span.SetStatus(codes.Ok, http.StatusText(204))
return nil
}
func encodeUpdateMapfixModelResponse(response *UpdateMapfixModelNoContent, w http.ResponseWriter, span trace.Span) error { func encodeUpdateMapfixModelResponse(response *UpdateMapfixModelNoContent, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(204) w.WriteHeader(204)
span.SetStatus(codes.Ok, http.StatusText(204)) span.SetStatus(codes.Ok, http.StatusText(204))

View File

@@ -216,6 +216,28 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
case 'd': // Prefix: "description"
if l := len("description"); len(elem) >= l && elem[0:l] == "description" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "PATCH":
s.handleUpdateMapfixDescriptionRequest([1]string{
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "PATCH")
}
return
}
case 'm': // Prefix: "model" case 'm': // Prefix: "model"
if l := len("model"); len(elem) >= l && elem[0:l] == "model" { if l := len("model"); len(elem) >= l && elem[0:l] == "model" {
@@ -939,6 +961,26 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
case 't': // Prefix: "tats"
if l := len("tats"); len(elem) >= l && elem[0:l] == "tats" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "GET":
s.handleGetStatsRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
}
return
}
case 'u': // Prefix: "ubmissions" case 'u': // Prefix: "ubmissions"
if l := len("ubmissions"); len(elem) >= l && elem[0:l] == "ubmissions" { if l := len("ubmissions"); len(elem) >= l && elem[0:l] == "ubmissions" {
@@ -1431,6 +1473,170 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
case 't': // Prefix: "thumbnails/"
if l := len("thumbnails/"); len(elem) >= l && elem[0:l] == "thumbnails/" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
break
}
switch elem[0] {
case 'a': // Prefix: "asset"
if l := len("asset"); len(elem) >= l && elem[0:l] == "asset" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
break
}
switch elem[0] {
case '/': // Prefix: "/"
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
elem = elem[l:]
} else {
break
}
// Param: "AssetID"
// Leaf parameter, slashes are prohibited
idx := strings.IndexByte(elem, '/')
if idx >= 0 {
break
}
args[0] = elem
elem = ""
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "GET":
s.handleGetAssetThumbnailRequest([1]string{
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
}
return
}
case 's': // Prefix: "s"
if l := len("s"); len(elem) >= l && elem[0:l] == "s" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "POST":
s.handleBatchAssetThumbnailsRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
}
return
}
}
case 'u': // Prefix: "user"
if l := len("user"); len(elem) >= l && elem[0:l] == "user" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
break
}
switch elem[0] {
case '/': // Prefix: "/"
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
elem = elem[l:]
} else {
break
}
// Param: "UserID"
// Leaf parameter, slashes are prohibited
idx := strings.IndexByte(elem, '/')
if idx >= 0 {
break
}
args[0] = elem
elem = ""
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "GET":
s.handleGetUserThumbnailRequest([1]string{
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
}
return
}
case 's': // Prefix: "s"
if l := len("s"); len(elem) >= l && elem[0:l] == "s" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "POST":
s.handleBatchUserThumbnailsRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
}
return
}
}
}
case 'u': // Prefix: "usernames"
if l := len("usernames"); len(elem) >= l && elem[0:l] == "usernames" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "POST":
s.handleBatchUsernamesRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
}
return
}
} }
} }
@@ -1440,12 +1646,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Route is route object. // Route is route object.
type Route struct { type Route struct {
name string name string
summary string summary string
operationID string operationID string
pathPattern string operationGroup string
count int pathPattern string
args [1]string count int
args [1]string
} }
// Name returns ogen operation name. // Name returns ogen operation name.
@@ -1465,6 +1672,11 @@ func (r Route) OperationID() string {
return r.operationID return r.operationID
} }
// OperationGroup returns the x-ogen-operation-group value.
func (r Route) OperationGroup() string {
return r.operationGroup
}
// PathPattern returns OpenAPI path. // PathPattern returns OpenAPI path.
func (r Route) PathPattern() string { func (r Route) PathPattern() string {
return r.pathPattern return r.pathPattern
@@ -1551,6 +1763,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ListMapfixesOperation r.name = ListMapfixesOperation
r.summary = "Get list of mapfixes" r.summary = "Get list of mapfixes"
r.operationID = "listMapfixes" r.operationID = "listMapfixes"
r.operationGroup = ""
r.pathPattern = "/mapfixes" r.pathPattern = "/mapfixes"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -1559,6 +1772,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = CreateMapfixOperation r.name = CreateMapfixOperation
r.summary = "Trigger the validator to create a mapfix" r.summary = "Trigger the validator to create a mapfix"
r.operationID = "createMapfix" r.operationID = "createMapfix"
r.operationGroup = ""
r.pathPattern = "/mapfixes" r.pathPattern = "/mapfixes"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -1591,6 +1805,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = GetMapfixOperation r.name = GetMapfixOperation
r.summary = "Retrieve map with ID" r.summary = "Retrieve map with ID"
r.operationID = "getMapfix" r.operationID = "getMapfix"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}" r.pathPattern = "/mapfixes/{MapfixID}"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1627,6 +1842,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ListMapfixAuditEventsOperation r.name = ListMapfixAuditEventsOperation
r.summary = "Retrieve a list of audit events" r.summary = "Retrieve a list of audit events"
r.operationID = "listMapfixAuditEvents" r.operationID = "listMapfixAuditEvents"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/audit-events" r.pathPattern = "/mapfixes/{MapfixID}/audit-events"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1663,6 +1879,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = CreateMapfixAuditCommentOperation r.name = CreateMapfixAuditCommentOperation
r.summary = "Post a comment to the audit log" r.summary = "Post a comment to the audit log"
r.operationID = "createMapfixAuditComment" r.operationID = "createMapfixAuditComment"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/comment" r.pathPattern = "/mapfixes/{MapfixID}/comment"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1687,6 +1904,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = SetMapfixCompletedOperation r.name = SetMapfixCompletedOperation
r.summary = "Called by maptest when a player completes the map" r.summary = "Called by maptest when a player completes the map"
r.operationID = "setMapfixCompleted" r.operationID = "setMapfixCompleted"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/completed" r.pathPattern = "/mapfixes/{MapfixID}/completed"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1698,6 +1916,31 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
} }
case 'd': // Prefix: "description"
if l := len("description"); len(elem) >= l && elem[0:l] == "description" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch method {
case "PATCH":
r.name = UpdateMapfixDescriptionOperation
r.summary = "Update description (submitter only)"
r.operationID = "updateMapfixDescription"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/description"
r.args = args
r.count = 1
return r, true
default:
return
}
}
case 'm': // Prefix: "model" case 'm': // Prefix: "model"
if l := len("model"); len(elem) >= l && elem[0:l] == "model" { if l := len("model"); len(elem) >= l && elem[0:l] == "model" {
@@ -1713,6 +1956,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = UpdateMapfixModelOperation r.name = UpdateMapfixModelOperation
r.summary = "Update model following role restrictions" r.summary = "Update model following role restrictions"
r.operationID = "updateMapfixModel" r.operationID = "updateMapfixModel"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/model" r.pathPattern = "/mapfixes/{MapfixID}/model"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1761,6 +2005,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixRejectOperation r.name = ActionMapfixRejectOperation
r.summary = "Role Reviewer changes status from Submitted -> Rejected" r.summary = "Role Reviewer changes status from Submitted -> Rejected"
r.operationID = "actionMapfixReject" r.operationID = "actionMapfixReject"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/reject" r.pathPattern = "/mapfixes/{MapfixID}/status/reject"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1785,6 +2030,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixRequestChangesOperation r.name = ActionMapfixRequestChangesOperation
r.summary = "Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested" r.summary = "Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested"
r.operationID = "actionMapfixRequestChanges" r.operationID = "actionMapfixRequestChanges"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/request-changes" r.pathPattern = "/mapfixes/{MapfixID}/status/request-changes"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1821,6 +2067,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixUploadedOperation r.name = ActionMapfixUploadedOperation
r.summary = "Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded" r.summary = "Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded"
r.operationID = "actionMapfixUploaded" r.operationID = "actionMapfixUploaded"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-releasing" r.pathPattern = "/mapfixes/{MapfixID}/status/reset-releasing"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1845,6 +2092,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixResetSubmittingOperation r.name = ActionMapfixResetSubmittingOperation
r.summary = "Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction" r.summary = "Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction"
r.operationID = "actionMapfixResetSubmitting" r.operationID = "actionMapfixResetSubmitting"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-submitting" r.pathPattern = "/mapfixes/{MapfixID}/status/reset-submitting"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1869,6 +2117,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixValidatedOperation r.name = ActionMapfixValidatedOperation
r.summary = "Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated" r.summary = "Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated"
r.operationID = "actionMapfixValidated" r.operationID = "actionMapfixValidated"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-uploading" r.pathPattern = "/mapfixes/{MapfixID}/status/reset-uploading"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1893,6 +2142,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixAcceptedOperation r.name = ActionMapfixAcceptedOperation
r.summary = "Role Reviewer manually resets validating softlock and changes status from Validating -> Accepted" r.summary = "Role Reviewer manually resets validating softlock and changes status from Validating -> Accepted"
r.operationID = "actionMapfixAccepted" r.operationID = "actionMapfixAccepted"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-validating" r.pathPattern = "/mapfixes/{MapfixID}/status/reset-validating"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1919,6 +2169,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixRetryValidateOperation r.name = ActionMapfixRetryValidateOperation
r.summary = "Role Reviewer re-runs validation and changes status from Accepted -> Validating" r.summary = "Role Reviewer re-runs validation and changes status from Accepted -> Validating"
r.operationID = "actionMapfixRetryValidate" r.operationID = "actionMapfixRetryValidate"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/retry-validate" r.pathPattern = "/mapfixes/{MapfixID}/status/retry-validate"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1943,6 +2194,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixRevokeOperation r.name = ActionMapfixRevokeOperation
r.summary = "Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction" r.summary = "Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction"
r.operationID = "actionMapfixRevoke" r.operationID = "actionMapfixRevoke"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/revoke" r.pathPattern = "/mapfixes/{MapfixID}/status/revoke"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -1981,6 +2233,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixTriggerReleaseOperation r.name = ActionMapfixTriggerReleaseOperation
r.summary = "Role MapfixUpload changes status from Uploaded -> Releasing" r.summary = "Role MapfixUpload changes status from Uploaded -> Releasing"
r.operationID = "actionMapfixTriggerRelease" r.operationID = "actionMapfixTriggerRelease"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-release" r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-release"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2004,6 +2257,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixTriggerSubmitOperation r.name = ActionMapfixTriggerSubmitOperation
r.summary = "Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting" r.summary = "Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting"
r.operationID = "actionMapfixTriggerSubmit" r.operationID = "actionMapfixTriggerSubmit"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-submit" r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-submit"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2028,6 +2282,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixTriggerSubmitUncheckedOperation r.name = ActionMapfixTriggerSubmitUncheckedOperation
r.summary = "Role Reviewer changes status from ChangesRequested -> Submitting" r.summary = "Role Reviewer changes status from ChangesRequested -> Submitting"
r.operationID = "actionMapfixTriggerSubmitUnchecked" r.operationID = "actionMapfixTriggerSubmitUnchecked"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-submit-unchecked" r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-submit-unchecked"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2054,6 +2309,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixTriggerUploadOperation r.name = ActionMapfixTriggerUploadOperation
r.summary = "Role MapfixUpload changes status from Validated -> Uploading" r.summary = "Role MapfixUpload changes status from Validated -> Uploading"
r.operationID = "actionMapfixTriggerUpload" r.operationID = "actionMapfixTriggerUpload"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-upload" r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-upload"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2078,6 +2334,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionMapfixTriggerValidateOperation r.name = ActionMapfixTriggerValidateOperation
r.summary = "Role Reviewer triggers validation and changes status from Submitted -> Validating" r.summary = "Role Reviewer triggers validation and changes status from Submitted -> Validating"
r.operationID = "actionMapfixTriggerValidate" r.operationID = "actionMapfixTriggerValidate"
r.operationGroup = ""
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-validate" r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-validate"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2111,6 +2368,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ListMapsOperation r.name = ListMapsOperation
r.summary = "Get list of maps" r.summary = "Get list of maps"
r.operationID = "listMaps" r.operationID = "listMaps"
r.operationGroup = ""
r.pathPattern = "/maps" r.pathPattern = "/maps"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2143,6 +2401,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = GetMapOperation r.name = GetMapOperation
r.summary = "Retrieve map with ID" r.summary = "Retrieve map with ID"
r.operationID = "getMap" r.operationID = "getMap"
r.operationGroup = ""
r.pathPattern = "/maps/{MapID}" r.pathPattern = "/maps/{MapID}"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2167,6 +2426,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = DownloadMapAssetOperation r.name = DownloadMapAssetOperation
r.summary = "Download the map asset" r.summary = "Download the map asset"
r.operationID = "downloadMapAsset" r.operationID = "downloadMapAsset"
r.operationGroup = ""
r.pathPattern = "/maps/{MapID}/download" r.pathPattern = "/maps/{MapID}/download"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2206,6 +2466,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = GetOperationOperation r.name = GetOperationOperation
r.summary = "Retrieve operation with ID" r.summary = "Retrieve operation with ID"
r.operationID = "getOperation" r.operationID = "getOperation"
r.operationGroup = ""
r.pathPattern = "/operations/{OperationID}" r.pathPattern = "/operations/{OperationID}"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2230,6 +2491,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ReleaseSubmissionsOperation r.name = ReleaseSubmissionsOperation
r.summary = "Release a set of uploaded maps. Role SubmissionRelease" r.summary = "Release a set of uploaded maps. Role SubmissionRelease"
r.operationID = "releaseSubmissions" r.operationID = "releaseSubmissions"
r.operationGroup = ""
r.pathPattern = "/release-submissions" r.pathPattern = "/release-submissions"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2277,6 +2539,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ListScriptPolicyOperation r.name = ListScriptPolicyOperation
r.summary = "Get list of script policies" r.summary = "Get list of script policies"
r.operationID = "listScriptPolicy" r.operationID = "listScriptPolicy"
r.operationGroup = ""
r.pathPattern = "/script-policy" r.pathPattern = "/script-policy"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2285,6 +2548,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = CreateScriptPolicyOperation r.name = CreateScriptPolicyOperation
r.summary = "Create a new script policy" r.summary = "Create a new script policy"
r.operationID = "createScriptPolicy" r.operationID = "createScriptPolicy"
r.operationGroup = ""
r.pathPattern = "/script-policy" r.pathPattern = "/script-policy"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2318,6 +2582,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = DeleteScriptPolicyOperation r.name = DeleteScriptPolicyOperation
r.summary = "Delete the specified script policy by ID" r.summary = "Delete the specified script policy by ID"
r.operationID = "deleteScriptPolicy" r.operationID = "deleteScriptPolicy"
r.operationGroup = ""
r.pathPattern = "/script-policy/{ScriptPolicyID}" r.pathPattern = "/script-policy/{ScriptPolicyID}"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2326,6 +2591,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = GetScriptPolicyOperation r.name = GetScriptPolicyOperation
r.summary = "Get the specified script policy by ID" r.summary = "Get the specified script policy by ID"
r.operationID = "getScriptPolicy" r.operationID = "getScriptPolicy"
r.operationGroup = ""
r.pathPattern = "/script-policy/{ScriptPolicyID}" r.pathPattern = "/script-policy/{ScriptPolicyID}"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2334,6 +2600,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = UpdateScriptPolicyOperation r.name = UpdateScriptPolicyOperation
r.summary = "Update the specified script policy by ID" r.summary = "Update the specified script policy by ID"
r.operationID = "updateScriptPolicy" r.operationID = "updateScriptPolicy"
r.operationGroup = ""
r.pathPattern = "/script-policy/{ScriptPolicyID}" r.pathPattern = "/script-policy/{ScriptPolicyID}"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2359,6 +2626,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ListScriptsOperation r.name = ListScriptsOperation
r.summary = "Get list of scripts" r.summary = "Get list of scripts"
r.operationID = "listScripts" r.operationID = "listScripts"
r.operationGroup = ""
r.pathPattern = "/scripts" r.pathPattern = "/scripts"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2367,6 +2635,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = CreateScriptOperation r.name = CreateScriptOperation
r.summary = "Create a new script" r.summary = "Create a new script"
r.operationID = "createScript" r.operationID = "createScript"
r.operationGroup = ""
r.pathPattern = "/scripts" r.pathPattern = "/scripts"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2400,6 +2669,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = DeleteScriptOperation r.name = DeleteScriptOperation
r.summary = "Delete the specified script by ID" r.summary = "Delete the specified script by ID"
r.operationID = "deleteScript" r.operationID = "deleteScript"
r.operationGroup = ""
r.pathPattern = "/scripts/{ScriptID}" r.pathPattern = "/scripts/{ScriptID}"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2408,6 +2678,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = GetScriptOperation r.name = GetScriptOperation
r.summary = "Get the specified script by ID" r.summary = "Get the specified script by ID"
r.operationID = "getScript" r.operationID = "getScript"
r.operationGroup = ""
r.pathPattern = "/scripts/{ScriptID}" r.pathPattern = "/scripts/{ScriptID}"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2416,6 +2687,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = UpdateScriptOperation r.name = UpdateScriptOperation
r.summary = "Update the specified script by ID" r.summary = "Update the specified script by ID"
r.operationID = "updateScript" r.operationID = "updateScript"
r.operationGroup = ""
r.pathPattern = "/scripts/{ScriptID}" r.pathPattern = "/scripts/{ScriptID}"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2456,6 +2728,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = SessionRolesOperation r.name = SessionRolesOperation
r.summary = "Get list of roles for the current session" r.summary = "Get list of roles for the current session"
r.operationID = "sessionRoles" r.operationID = "sessionRoles"
r.operationGroup = ""
r.pathPattern = "/session/roles" r.pathPattern = "/session/roles"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2480,6 +2753,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = SessionUserOperation r.name = SessionUserOperation
r.summary = "Get information about the currently logged in user" r.summary = "Get information about the currently logged in user"
r.operationID = "sessionUser" r.operationID = "sessionUser"
r.operationGroup = ""
r.pathPattern = "/session/user" r.pathPattern = "/session/user"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2504,6 +2778,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = SessionValidateOperation r.name = SessionValidateOperation
r.summary = "Ask if the current session is valid" r.summary = "Ask if the current session is valid"
r.operationID = "sessionValidate" r.operationID = "sessionValidate"
r.operationGroup = ""
r.pathPattern = "/session/validate" r.pathPattern = "/session/validate"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2515,6 +2790,31 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
} }
case 't': // Prefix: "tats"
if l := len("tats"); len(elem) >= l && elem[0:l] == "tats" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch method {
case "GET":
r.name = GetStatsOperation
r.summary = "Get aggregate statistics"
r.operationID = "getStats"
r.operationGroup = ""
r.pathPattern = "/stats"
r.args = args
r.count = 0
return r, true
default:
return
}
}
case 'u': // Prefix: "ubmissions" case 'u': // Prefix: "ubmissions"
if l := len("ubmissions"); len(elem) >= l && elem[0:l] == "ubmissions" { if l := len("ubmissions"); len(elem) >= l && elem[0:l] == "ubmissions" {
@@ -2529,6 +2829,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ListSubmissionsOperation r.name = ListSubmissionsOperation
r.summary = "Get list of submissions" r.summary = "Get list of submissions"
r.operationID = "listSubmissions" r.operationID = "listSubmissions"
r.operationGroup = ""
r.pathPattern = "/submissions" r.pathPattern = "/submissions"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2537,6 +2838,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = CreateSubmissionOperation r.name = CreateSubmissionOperation
r.summary = "Trigger the validator to create a new submission" r.summary = "Trigger the validator to create a new submission"
r.operationID = "createSubmission" r.operationID = "createSubmission"
r.operationGroup = ""
r.pathPattern = "/submissions" r.pathPattern = "/submissions"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2561,6 +2863,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = CreateSubmissionAdminOperation r.name = CreateSubmissionAdminOperation
r.summary = "Trigger the validator to create a new submission" r.summary = "Trigger the validator to create a new submission"
r.operationID = "createSubmissionAdmin" r.operationID = "createSubmissionAdmin"
r.operationGroup = ""
r.pathPattern = "/submissions-admin" r.pathPattern = "/submissions-admin"
r.args = args r.args = args
r.count = 0 r.count = 0
@@ -2593,6 +2896,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = GetSubmissionOperation r.name = GetSubmissionOperation
r.summary = "Retrieve map with ID" r.summary = "Retrieve map with ID"
r.operationID = "getSubmission" r.operationID = "getSubmission"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}" r.pathPattern = "/submissions/{SubmissionID}"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2629,6 +2933,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ListSubmissionAuditEventsOperation r.name = ListSubmissionAuditEventsOperation
r.summary = "Retrieve a list of audit events" r.summary = "Retrieve a list of audit events"
r.operationID = "listSubmissionAuditEvents" r.operationID = "listSubmissionAuditEvents"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/audit-events" r.pathPattern = "/submissions/{SubmissionID}/audit-events"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2665,6 +2970,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = CreateSubmissionAuditCommentOperation r.name = CreateSubmissionAuditCommentOperation
r.summary = "Post a comment to the audit log" r.summary = "Post a comment to the audit log"
r.operationID = "createSubmissionAuditComment" r.operationID = "createSubmissionAuditComment"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/comment" r.pathPattern = "/submissions/{SubmissionID}/comment"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2689,6 +2995,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = SetSubmissionCompletedOperation r.name = SetSubmissionCompletedOperation
r.summary = "Called by maptest when a player completes the map" r.summary = "Called by maptest when a player completes the map"
r.operationID = "setSubmissionCompleted" r.operationID = "setSubmissionCompleted"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/completed" r.pathPattern = "/submissions/{SubmissionID}/completed"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2715,6 +3022,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = UpdateSubmissionModelOperation r.name = UpdateSubmissionModelOperation
r.summary = "Update model following role restrictions" r.summary = "Update model following role restrictions"
r.operationID = "updateSubmissionModel" r.operationID = "updateSubmissionModel"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/model" r.pathPattern = "/submissions/{SubmissionID}/model"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2763,6 +3071,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionSubmissionRejectOperation r.name = ActionSubmissionRejectOperation
r.summary = "Role Reviewer changes status from Submitted -> Rejected" r.summary = "Role Reviewer changes status from Submitted -> Rejected"
r.operationID = "actionSubmissionReject" r.operationID = "actionSubmissionReject"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/status/reject" r.pathPattern = "/submissions/{SubmissionID}/status/reject"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2787,6 +3096,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionSubmissionRequestChangesOperation r.name = ActionSubmissionRequestChangesOperation
r.summary = "Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested" r.summary = "Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested"
r.operationID = "actionSubmissionRequestChanges" r.operationID = "actionSubmissionRequestChanges"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/status/request-changes" r.pathPattern = "/submissions/{SubmissionID}/status/request-changes"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2823,6 +3133,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionSubmissionResetSubmittingOperation r.name = ActionSubmissionResetSubmittingOperation
r.summary = "Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction" r.summary = "Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction"
r.operationID = "actionSubmissionResetSubmitting" r.operationID = "actionSubmissionResetSubmitting"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/status/reset-submitting" r.pathPattern = "/submissions/{SubmissionID}/status/reset-submitting"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2847,6 +3158,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionSubmissionValidatedOperation r.name = ActionSubmissionValidatedOperation
r.summary = "Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> Validated" r.summary = "Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> Validated"
r.operationID = "actionSubmissionValidated" r.operationID = "actionSubmissionValidated"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/status/reset-uploading" r.pathPattern = "/submissions/{SubmissionID}/status/reset-uploading"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2871,6 +3183,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionSubmissionAcceptedOperation r.name = ActionSubmissionAcceptedOperation
r.summary = "Role Reviewer manually resets validating softlock and changes status from Validating -> Accepted" r.summary = "Role Reviewer manually resets validating softlock and changes status from Validating -> Accepted"
r.operationID = "actionSubmissionAccepted" r.operationID = "actionSubmissionAccepted"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/status/reset-validating" r.pathPattern = "/submissions/{SubmissionID}/status/reset-validating"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2897,6 +3210,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionSubmissionRetryValidateOperation r.name = ActionSubmissionRetryValidateOperation
r.summary = "Role Reviewer re-runs validation and changes status from Accepted -> Validating" r.summary = "Role Reviewer re-runs validation and changes status from Accepted -> Validating"
r.operationID = "actionSubmissionRetryValidate" r.operationID = "actionSubmissionRetryValidate"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/status/retry-validate" r.pathPattern = "/submissions/{SubmissionID}/status/retry-validate"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2921,6 +3235,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionSubmissionRevokeOperation r.name = ActionSubmissionRevokeOperation
r.summary = "Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction" r.summary = "Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction"
r.operationID = "actionSubmissionRevoke" r.operationID = "actionSubmissionRevoke"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/status/revoke" r.pathPattern = "/submissions/{SubmissionID}/status/revoke"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2958,6 +3273,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionSubmissionTriggerSubmitOperation r.name = ActionSubmissionTriggerSubmitOperation
r.summary = "Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting" r.summary = "Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting"
r.operationID = "actionSubmissionTriggerSubmit" r.operationID = "actionSubmissionTriggerSubmit"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-submit" r.pathPattern = "/submissions/{SubmissionID}/status/trigger-submit"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -2982,6 +3298,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionSubmissionTriggerSubmitUncheckedOperation r.name = ActionSubmissionTriggerSubmitUncheckedOperation
r.summary = "Role Reviewer changes status from ChangesRequested -> Submitting" r.summary = "Role Reviewer changes status from ChangesRequested -> Submitting"
r.operationID = "actionSubmissionTriggerSubmitUnchecked" r.operationID = "actionSubmissionTriggerSubmitUnchecked"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-submit-unchecked" r.pathPattern = "/submissions/{SubmissionID}/status/trigger-submit-unchecked"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -3008,6 +3325,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionSubmissionTriggerUploadOperation r.name = ActionSubmissionTriggerUploadOperation
r.summary = "Role SubmissionUpload changes status from Validated -> Uploading" r.summary = "Role SubmissionUpload changes status from Validated -> Uploading"
r.operationID = "actionSubmissionTriggerUpload" r.operationID = "actionSubmissionTriggerUpload"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-upload" r.pathPattern = "/submissions/{SubmissionID}/status/trigger-upload"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -3032,6 +3350,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ActionSubmissionTriggerValidateOperation r.name = ActionSubmissionTriggerValidateOperation
r.summary = "Role Reviewer triggers validation and changes status from Submitted -> Validating" r.summary = "Role Reviewer triggers validation and changes status from Submitted -> Validating"
r.operationID = "actionSubmissionTriggerValidate" r.operationID = "actionSubmissionTriggerValidate"
r.operationGroup = ""
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-validate" r.pathPattern = "/submissions/{SubmissionID}/status/trigger-validate"
r.args = args r.args = args
r.count = 1 r.count = 1
@@ -3053,6 +3372,191 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
} }
case 't': // Prefix: "thumbnails/"
if l := len("thumbnails/"); len(elem) >= l && elem[0:l] == "thumbnails/" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
break
}
switch elem[0] {
case 'a': // Prefix: "asset"
if l := len("asset"); len(elem) >= l && elem[0:l] == "asset" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
break
}
switch elem[0] {
case '/': // Prefix: "/"
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
elem = elem[l:]
} else {
break
}
// Param: "AssetID"
// Leaf parameter, slashes are prohibited
idx := strings.IndexByte(elem, '/')
if idx >= 0 {
break
}
args[0] = elem
elem = ""
if len(elem) == 0 {
// Leaf node.
switch method {
case "GET":
r.name = GetAssetThumbnailOperation
r.summary = "Get single asset thumbnail"
r.operationID = "getAssetThumbnail"
r.operationGroup = ""
r.pathPattern = "/thumbnails/asset/{AssetID}"
r.args = args
r.count = 1
return r, true
default:
return
}
}
case 's': // Prefix: "s"
if l := len("s"); len(elem) >= l && elem[0:l] == "s" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch method {
case "POST":
r.name = BatchAssetThumbnailsOperation
r.summary = "Batch fetch asset thumbnails"
r.operationID = "batchAssetThumbnails"
r.operationGroup = ""
r.pathPattern = "/thumbnails/assets"
r.args = args
r.count = 0
return r, true
default:
return
}
}
}
case 'u': // Prefix: "user"
if l := len("user"); len(elem) >= l && elem[0:l] == "user" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
break
}
switch elem[0] {
case '/': // Prefix: "/"
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
elem = elem[l:]
} else {
break
}
// Param: "UserID"
// Leaf parameter, slashes are prohibited
idx := strings.IndexByte(elem, '/')
if idx >= 0 {
break
}
args[0] = elem
elem = ""
if len(elem) == 0 {
// Leaf node.
switch method {
case "GET":
r.name = GetUserThumbnailOperation
r.summary = "Get single user avatar thumbnail"
r.operationID = "getUserThumbnail"
r.operationGroup = ""
r.pathPattern = "/thumbnails/user/{UserID}"
r.args = args
r.count = 1
return r, true
default:
return
}
}
case 's': // Prefix: "s"
if l := len("s"); len(elem) >= l && elem[0:l] == "s" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch method {
case "POST":
r.name = BatchUserThumbnailsOperation
r.summary = "Batch fetch user avatar thumbnails"
r.operationID = "batchUserThumbnails"
r.operationGroup = ""
r.pathPattern = "/thumbnails/users"
r.args = args
r.count = 0
return r, true
default:
return
}
}
}
}
case 'u': // Prefix: "usernames"
if l := len("usernames"); len(elem) >= l && elem[0:l] == "usernames" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch method {
case "POST":
r.name = BatchUsernamesOperation
r.summary = "Batch fetch usernames"
r.operationID = "batchUsernames"
r.operationGroup = ""
r.pathPattern = "/usernames"
r.args = args
r.count = 0
return r, true
default:
return
}
}
} }
} }

View File

@@ -7,6 +7,7 @@ import (
"io" "io"
"time" "time"
"github.com/go-faster/errors"
"github.com/go-faster/jx" "github.com/go-faster/jx"
) )
@@ -192,6 +193,254 @@ func (s *AuditEventEventData) init() AuditEventEventData {
return m return m
} }
type BatchAssetThumbnailsOK struct {
// Map of asset ID to thumbnail URL.
Thumbnails OptBatchAssetThumbnailsOKThumbnails `json:"thumbnails"`
}
// GetThumbnails returns the value of Thumbnails.
func (s *BatchAssetThumbnailsOK) GetThumbnails() OptBatchAssetThumbnailsOKThumbnails {
return s.Thumbnails
}
// SetThumbnails sets the value of Thumbnails.
func (s *BatchAssetThumbnailsOK) SetThumbnails(val OptBatchAssetThumbnailsOKThumbnails) {
s.Thumbnails = val
}
// Map of asset ID to thumbnail URL.
type BatchAssetThumbnailsOKThumbnails map[string]string
func (s *BatchAssetThumbnailsOKThumbnails) init() BatchAssetThumbnailsOKThumbnails {
m := *s
if m == nil {
m = map[string]string{}
*s = m
}
return m
}
type BatchAssetThumbnailsReq struct {
// Array of asset IDs (max 100).
AssetIds []uint64 `json:"assetIds"`
// Thumbnail size.
Size OptBatchAssetThumbnailsReqSize `json:"size"`
}
// GetAssetIds returns the value of AssetIds.
func (s *BatchAssetThumbnailsReq) GetAssetIds() []uint64 {
return s.AssetIds
}
// GetSize returns the value of Size.
func (s *BatchAssetThumbnailsReq) GetSize() OptBatchAssetThumbnailsReqSize {
return s.Size
}
// SetAssetIds sets the value of AssetIds.
func (s *BatchAssetThumbnailsReq) SetAssetIds(val []uint64) {
s.AssetIds = val
}
// SetSize sets the value of Size.
func (s *BatchAssetThumbnailsReq) SetSize(val OptBatchAssetThumbnailsReqSize) {
s.Size = val
}
// Thumbnail size.
type BatchAssetThumbnailsReqSize string
const (
BatchAssetThumbnailsReqSize150x150 BatchAssetThumbnailsReqSize = "150x150"
BatchAssetThumbnailsReqSize420x420 BatchAssetThumbnailsReqSize = "420x420"
BatchAssetThumbnailsReqSize768x432 BatchAssetThumbnailsReqSize = "768x432"
)
// AllValues returns all BatchAssetThumbnailsReqSize values.
func (BatchAssetThumbnailsReqSize) AllValues() []BatchAssetThumbnailsReqSize {
return []BatchAssetThumbnailsReqSize{
BatchAssetThumbnailsReqSize150x150,
BatchAssetThumbnailsReqSize420x420,
BatchAssetThumbnailsReqSize768x432,
}
}
// MarshalText implements encoding.TextMarshaler.
func (s BatchAssetThumbnailsReqSize) MarshalText() ([]byte, error) {
switch s {
case BatchAssetThumbnailsReqSize150x150:
return []byte(s), nil
case BatchAssetThumbnailsReqSize420x420:
return []byte(s), nil
case BatchAssetThumbnailsReqSize768x432:
return []byte(s), nil
default:
return nil, errors.Errorf("invalid value: %q", s)
}
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (s *BatchAssetThumbnailsReqSize) UnmarshalText(data []byte) error {
switch BatchAssetThumbnailsReqSize(data) {
case BatchAssetThumbnailsReqSize150x150:
*s = BatchAssetThumbnailsReqSize150x150
return nil
case BatchAssetThumbnailsReqSize420x420:
*s = BatchAssetThumbnailsReqSize420x420
return nil
case BatchAssetThumbnailsReqSize768x432:
*s = BatchAssetThumbnailsReqSize768x432
return nil
default:
return errors.Errorf("invalid value: %q", data)
}
}
type BatchUserThumbnailsOK struct {
// Map of user ID to thumbnail URL.
Thumbnails OptBatchUserThumbnailsOKThumbnails `json:"thumbnails"`
}
// GetThumbnails returns the value of Thumbnails.
func (s *BatchUserThumbnailsOK) GetThumbnails() OptBatchUserThumbnailsOKThumbnails {
return s.Thumbnails
}
// SetThumbnails sets the value of Thumbnails.
func (s *BatchUserThumbnailsOK) SetThumbnails(val OptBatchUserThumbnailsOKThumbnails) {
s.Thumbnails = val
}
// Map of user ID to thumbnail URL.
type BatchUserThumbnailsOKThumbnails map[string]string
func (s *BatchUserThumbnailsOKThumbnails) init() BatchUserThumbnailsOKThumbnails {
m := *s
if m == nil {
m = map[string]string{}
*s = m
}
return m
}
type BatchUserThumbnailsReq struct {
// Array of user IDs (max 100).
UserIds []uint64 `json:"userIds"`
// Thumbnail size.
Size OptBatchUserThumbnailsReqSize `json:"size"`
}
// GetUserIds returns the value of UserIds.
func (s *BatchUserThumbnailsReq) GetUserIds() []uint64 {
return s.UserIds
}
// GetSize returns the value of Size.
func (s *BatchUserThumbnailsReq) GetSize() OptBatchUserThumbnailsReqSize {
return s.Size
}
// SetUserIds sets the value of UserIds.
func (s *BatchUserThumbnailsReq) SetUserIds(val []uint64) {
s.UserIds = val
}
// SetSize sets the value of Size.
func (s *BatchUserThumbnailsReq) SetSize(val OptBatchUserThumbnailsReqSize) {
s.Size = val
}
// Thumbnail size.
type BatchUserThumbnailsReqSize string
const (
BatchUserThumbnailsReqSize150x150 BatchUserThumbnailsReqSize = "150x150"
BatchUserThumbnailsReqSize420x420 BatchUserThumbnailsReqSize = "420x420"
BatchUserThumbnailsReqSize768x432 BatchUserThumbnailsReqSize = "768x432"
)
// AllValues returns all BatchUserThumbnailsReqSize values.
func (BatchUserThumbnailsReqSize) AllValues() []BatchUserThumbnailsReqSize {
return []BatchUserThumbnailsReqSize{
BatchUserThumbnailsReqSize150x150,
BatchUserThumbnailsReqSize420x420,
BatchUserThumbnailsReqSize768x432,
}
}
// MarshalText implements encoding.TextMarshaler.
func (s BatchUserThumbnailsReqSize) MarshalText() ([]byte, error) {
switch s {
case BatchUserThumbnailsReqSize150x150:
return []byte(s), nil
case BatchUserThumbnailsReqSize420x420:
return []byte(s), nil
case BatchUserThumbnailsReqSize768x432:
return []byte(s), nil
default:
return nil, errors.Errorf("invalid value: %q", s)
}
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (s *BatchUserThumbnailsReqSize) UnmarshalText(data []byte) error {
switch BatchUserThumbnailsReqSize(data) {
case BatchUserThumbnailsReqSize150x150:
*s = BatchUserThumbnailsReqSize150x150
return nil
case BatchUserThumbnailsReqSize420x420:
*s = BatchUserThumbnailsReqSize420x420
return nil
case BatchUserThumbnailsReqSize768x432:
*s = BatchUserThumbnailsReqSize768x432
return nil
default:
return errors.Errorf("invalid value: %q", data)
}
}
type BatchUsernamesOK struct {
// Map of user ID to username.
Usernames OptBatchUsernamesOKUsernames `json:"usernames"`
}
// GetUsernames returns the value of Usernames.
func (s *BatchUsernamesOK) GetUsernames() OptBatchUsernamesOKUsernames {
return s.Usernames
}
// SetUsernames sets the value of Usernames.
func (s *BatchUsernamesOK) SetUsernames(val OptBatchUsernamesOKUsernames) {
s.Usernames = val
}
// Map of user ID to username.
type BatchUsernamesOKUsernames map[string]string
func (s *BatchUsernamesOKUsernames) init() BatchUsernamesOKUsernames {
m := *s
if m == nil {
m = map[string]string{}
*s = m
}
return m
}
type BatchUsernamesReq struct {
// Array of user IDs (max 100).
UserIds []uint64 `json:"userIds"`
}
// GetUserIds returns the value of UserIds.
func (s *BatchUsernamesReq) GetUserIds() []uint64 {
return s.UserIds
}
// SetUserIds sets the value of UserIds.
func (s *BatchUsernamesReq) SetUserIds(val []uint64) {
s.UserIds = val
}
type CookieAuth struct { type CookieAuth struct {
APIKey string APIKey string
Roles []string Roles []string
@@ -324,6 +573,132 @@ func (s *ErrorStatusCode) SetResponse(val Error) {
s.Response = val s.Response = val
} }
// GetAssetThumbnailFound is response for GetAssetThumbnail operation.
type GetAssetThumbnailFound struct {
Location OptString
}
// GetLocation returns the value of Location.
func (s *GetAssetThumbnailFound) GetLocation() OptString {
return s.Location
}
// SetLocation sets the value of Location.
func (s *GetAssetThumbnailFound) SetLocation(val OptString) {
s.Location = val
}
type GetAssetThumbnailSize string
const (
GetAssetThumbnailSize150x150 GetAssetThumbnailSize = "150x150"
GetAssetThumbnailSize420x420 GetAssetThumbnailSize = "420x420"
GetAssetThumbnailSize768x432 GetAssetThumbnailSize = "768x432"
)
// AllValues returns all GetAssetThumbnailSize values.
func (GetAssetThumbnailSize) AllValues() []GetAssetThumbnailSize {
return []GetAssetThumbnailSize{
GetAssetThumbnailSize150x150,
GetAssetThumbnailSize420x420,
GetAssetThumbnailSize768x432,
}
}
// MarshalText implements encoding.TextMarshaler.
func (s GetAssetThumbnailSize) MarshalText() ([]byte, error) {
switch s {
case GetAssetThumbnailSize150x150:
return []byte(s), nil
case GetAssetThumbnailSize420x420:
return []byte(s), nil
case GetAssetThumbnailSize768x432:
return []byte(s), nil
default:
return nil, errors.Errorf("invalid value: %q", s)
}
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (s *GetAssetThumbnailSize) UnmarshalText(data []byte) error {
switch GetAssetThumbnailSize(data) {
case GetAssetThumbnailSize150x150:
*s = GetAssetThumbnailSize150x150
return nil
case GetAssetThumbnailSize420x420:
*s = GetAssetThumbnailSize420x420
return nil
case GetAssetThumbnailSize768x432:
*s = GetAssetThumbnailSize768x432
return nil
default:
return errors.Errorf("invalid value: %q", data)
}
}
// GetUserThumbnailFound is response for GetUserThumbnail operation.
type GetUserThumbnailFound struct {
Location OptString
}
// GetLocation returns the value of Location.
func (s *GetUserThumbnailFound) GetLocation() OptString {
return s.Location
}
// SetLocation sets the value of Location.
func (s *GetUserThumbnailFound) SetLocation(val OptString) {
s.Location = val
}
type GetUserThumbnailSize string
const (
GetUserThumbnailSize150x150 GetUserThumbnailSize = "150x150"
GetUserThumbnailSize420x420 GetUserThumbnailSize = "420x420"
GetUserThumbnailSize768x432 GetUserThumbnailSize = "768x432"
)
// AllValues returns all GetUserThumbnailSize values.
func (GetUserThumbnailSize) AllValues() []GetUserThumbnailSize {
return []GetUserThumbnailSize{
GetUserThumbnailSize150x150,
GetUserThumbnailSize420x420,
GetUserThumbnailSize768x432,
}
}
// MarshalText implements encoding.TextMarshaler.
func (s GetUserThumbnailSize) MarshalText() ([]byte, error) {
switch s {
case GetUserThumbnailSize150x150:
return []byte(s), nil
case GetUserThumbnailSize420x420:
return []byte(s), nil
case GetUserThumbnailSize768x432:
return []byte(s), nil
default:
return nil, errors.Errorf("invalid value: %q", s)
}
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (s *GetUserThumbnailSize) UnmarshalText(data []byte) error {
switch GetUserThumbnailSize(data) {
case GetUserThumbnailSize150x150:
*s = GetUserThumbnailSize150x150
return nil
case GetUserThumbnailSize420x420:
*s = GetUserThumbnailSize420x420
return nil
case GetUserThumbnailSize768x432:
*s = GetUserThumbnailSize768x432
return nil
default:
return errors.Errorf("invalid value: %q", data)
}
}
// Ref: #/components/schemas/Map // Ref: #/components/schemas/Map
type Map struct { type Map struct {
ID int64 `json:"ID"` ID int64 `json:"ID"`
@@ -777,6 +1152,328 @@ func (s *OperationID) SetOperationID(val int32) {
s.OperationID = val s.OperationID = val
} }
// NewOptBatchAssetThumbnailsOKThumbnails returns new OptBatchAssetThumbnailsOKThumbnails with value set to v.
func NewOptBatchAssetThumbnailsOKThumbnails(v BatchAssetThumbnailsOKThumbnails) OptBatchAssetThumbnailsOKThumbnails {
return OptBatchAssetThumbnailsOKThumbnails{
Value: v,
Set: true,
}
}
// OptBatchAssetThumbnailsOKThumbnails is optional BatchAssetThumbnailsOKThumbnails.
type OptBatchAssetThumbnailsOKThumbnails struct {
Value BatchAssetThumbnailsOKThumbnails
Set bool
}
// IsSet returns true if OptBatchAssetThumbnailsOKThumbnails was set.
func (o OptBatchAssetThumbnailsOKThumbnails) IsSet() bool { return o.Set }
// Reset unsets value.
func (o *OptBatchAssetThumbnailsOKThumbnails) Reset() {
var v BatchAssetThumbnailsOKThumbnails
o.Value = v
o.Set = false
}
// SetTo sets value to v.
func (o *OptBatchAssetThumbnailsOKThumbnails) SetTo(v BatchAssetThumbnailsOKThumbnails) {
o.Set = true
o.Value = v
}
// Get returns value and boolean that denotes whether value was set.
func (o OptBatchAssetThumbnailsOKThumbnails) Get() (v BatchAssetThumbnailsOKThumbnails, ok bool) {
if !o.Set {
return v, false
}
return o.Value, true
}
// Or returns value if set, or given parameter if does not.
func (o OptBatchAssetThumbnailsOKThumbnails) Or(d BatchAssetThumbnailsOKThumbnails) BatchAssetThumbnailsOKThumbnails {
if v, ok := o.Get(); ok {
return v
}
return d
}
// NewOptBatchAssetThumbnailsReqSize returns new OptBatchAssetThumbnailsReqSize with value set to v.
func NewOptBatchAssetThumbnailsReqSize(v BatchAssetThumbnailsReqSize) OptBatchAssetThumbnailsReqSize {
return OptBatchAssetThumbnailsReqSize{
Value: v,
Set: true,
}
}
// OptBatchAssetThumbnailsReqSize is optional BatchAssetThumbnailsReqSize.
type OptBatchAssetThumbnailsReqSize struct {
Value BatchAssetThumbnailsReqSize
Set bool
}
// IsSet returns true if OptBatchAssetThumbnailsReqSize was set.
func (o OptBatchAssetThumbnailsReqSize) IsSet() bool { return o.Set }
// Reset unsets value.
func (o *OptBatchAssetThumbnailsReqSize) Reset() {
var v BatchAssetThumbnailsReqSize
o.Value = v
o.Set = false
}
// SetTo sets value to v.
func (o *OptBatchAssetThumbnailsReqSize) SetTo(v BatchAssetThumbnailsReqSize) {
o.Set = true
o.Value = v
}
// Get returns value and boolean that denotes whether value was set.
func (o OptBatchAssetThumbnailsReqSize) Get() (v BatchAssetThumbnailsReqSize, ok bool) {
if !o.Set {
return v, false
}
return o.Value, true
}
// Or returns value if set, or given parameter if does not.
func (o OptBatchAssetThumbnailsReqSize) Or(d BatchAssetThumbnailsReqSize) BatchAssetThumbnailsReqSize {
if v, ok := o.Get(); ok {
return v
}
return d
}
// NewOptBatchUserThumbnailsOKThumbnails returns new OptBatchUserThumbnailsOKThumbnails with value set to v.
func NewOptBatchUserThumbnailsOKThumbnails(v BatchUserThumbnailsOKThumbnails) OptBatchUserThumbnailsOKThumbnails {
return OptBatchUserThumbnailsOKThumbnails{
Value: v,
Set: true,
}
}
// OptBatchUserThumbnailsOKThumbnails is optional BatchUserThumbnailsOKThumbnails.
type OptBatchUserThumbnailsOKThumbnails struct {
Value BatchUserThumbnailsOKThumbnails
Set bool
}
// IsSet returns true if OptBatchUserThumbnailsOKThumbnails was set.
func (o OptBatchUserThumbnailsOKThumbnails) IsSet() bool { return o.Set }
// Reset unsets value.
func (o *OptBatchUserThumbnailsOKThumbnails) Reset() {
var v BatchUserThumbnailsOKThumbnails
o.Value = v
o.Set = false
}
// SetTo sets value to v.
func (o *OptBatchUserThumbnailsOKThumbnails) SetTo(v BatchUserThumbnailsOKThumbnails) {
o.Set = true
o.Value = v
}
// Get returns value and boolean that denotes whether value was set.
func (o OptBatchUserThumbnailsOKThumbnails) Get() (v BatchUserThumbnailsOKThumbnails, ok bool) {
if !o.Set {
return v, false
}
return o.Value, true
}
// Or returns value if set, or given parameter if does not.
func (o OptBatchUserThumbnailsOKThumbnails) Or(d BatchUserThumbnailsOKThumbnails) BatchUserThumbnailsOKThumbnails {
if v, ok := o.Get(); ok {
return v
}
return d
}
// NewOptBatchUserThumbnailsReqSize returns new OptBatchUserThumbnailsReqSize with value set to v.
func NewOptBatchUserThumbnailsReqSize(v BatchUserThumbnailsReqSize) OptBatchUserThumbnailsReqSize {
return OptBatchUserThumbnailsReqSize{
Value: v,
Set: true,
}
}
// OptBatchUserThumbnailsReqSize is optional BatchUserThumbnailsReqSize.
type OptBatchUserThumbnailsReqSize struct {
Value BatchUserThumbnailsReqSize
Set bool
}
// IsSet returns true if OptBatchUserThumbnailsReqSize was set.
func (o OptBatchUserThumbnailsReqSize) IsSet() bool { return o.Set }
// Reset unsets value.
func (o *OptBatchUserThumbnailsReqSize) Reset() {
var v BatchUserThumbnailsReqSize
o.Value = v
o.Set = false
}
// SetTo sets value to v.
func (o *OptBatchUserThumbnailsReqSize) SetTo(v BatchUserThumbnailsReqSize) {
o.Set = true
o.Value = v
}
// Get returns value and boolean that denotes whether value was set.
func (o OptBatchUserThumbnailsReqSize) Get() (v BatchUserThumbnailsReqSize, ok bool) {
if !o.Set {
return v, false
}
return o.Value, true
}
// Or returns value if set, or given parameter if does not.
func (o OptBatchUserThumbnailsReqSize) Or(d BatchUserThumbnailsReqSize) BatchUserThumbnailsReqSize {
if v, ok := o.Get(); ok {
return v
}
return d
}
// NewOptBatchUsernamesOKUsernames returns new OptBatchUsernamesOKUsernames with value set to v.
func NewOptBatchUsernamesOKUsernames(v BatchUsernamesOKUsernames) OptBatchUsernamesOKUsernames {
return OptBatchUsernamesOKUsernames{
Value: v,
Set: true,
}
}
// OptBatchUsernamesOKUsernames is optional BatchUsernamesOKUsernames.
type OptBatchUsernamesOKUsernames struct {
Value BatchUsernamesOKUsernames
Set bool
}
// IsSet returns true if OptBatchUsernamesOKUsernames was set.
func (o OptBatchUsernamesOKUsernames) IsSet() bool { return o.Set }
// Reset unsets value.
func (o *OptBatchUsernamesOKUsernames) Reset() {
var v BatchUsernamesOKUsernames
o.Value = v
o.Set = false
}
// SetTo sets value to v.
func (o *OptBatchUsernamesOKUsernames) SetTo(v BatchUsernamesOKUsernames) {
o.Set = true
o.Value = v
}
// Get returns value and boolean that denotes whether value was set.
func (o OptBatchUsernamesOKUsernames) Get() (v BatchUsernamesOKUsernames, ok bool) {
if !o.Set {
return v, false
}
return o.Value, true
}
// Or returns value if set, or given parameter if does not.
func (o OptBatchUsernamesOKUsernames) Or(d BatchUsernamesOKUsernames) BatchUsernamesOKUsernames {
if v, ok := o.Get(); ok {
return v
}
return d
}
// NewOptGetAssetThumbnailSize returns new OptGetAssetThumbnailSize with value set to v.
func NewOptGetAssetThumbnailSize(v GetAssetThumbnailSize) OptGetAssetThumbnailSize {
return OptGetAssetThumbnailSize{
Value: v,
Set: true,
}
}
// OptGetAssetThumbnailSize is optional GetAssetThumbnailSize.
type OptGetAssetThumbnailSize struct {
Value GetAssetThumbnailSize
Set bool
}
// IsSet returns true if OptGetAssetThumbnailSize was set.
func (o OptGetAssetThumbnailSize) IsSet() bool { return o.Set }
// Reset unsets value.
func (o *OptGetAssetThumbnailSize) Reset() {
var v GetAssetThumbnailSize
o.Value = v
o.Set = false
}
// SetTo sets value to v.
func (o *OptGetAssetThumbnailSize) SetTo(v GetAssetThumbnailSize) {
o.Set = true
o.Value = v
}
// Get returns value and boolean that denotes whether value was set.
func (o OptGetAssetThumbnailSize) Get() (v GetAssetThumbnailSize, ok bool) {
if !o.Set {
return v, false
}
return o.Value, true
}
// Or returns value if set, or given parameter if does not.
func (o OptGetAssetThumbnailSize) Or(d GetAssetThumbnailSize) GetAssetThumbnailSize {
if v, ok := o.Get(); ok {
return v
}
return d
}
// NewOptGetUserThumbnailSize returns new OptGetUserThumbnailSize with value set to v.
func NewOptGetUserThumbnailSize(v GetUserThumbnailSize) OptGetUserThumbnailSize {
return OptGetUserThumbnailSize{
Value: v,
Set: true,
}
}
// OptGetUserThumbnailSize is optional GetUserThumbnailSize.
type OptGetUserThumbnailSize struct {
Value GetUserThumbnailSize
Set bool
}
// IsSet returns true if OptGetUserThumbnailSize was set.
func (o OptGetUserThumbnailSize) IsSet() bool { return o.Set }
// Reset unsets value.
func (o *OptGetUserThumbnailSize) Reset() {
var v GetUserThumbnailSize
o.Value = v
o.Set = false
}
// SetTo sets value to v.
func (o *OptGetUserThumbnailSize) SetTo(v GetUserThumbnailSize) {
o.Set = true
o.Value = v
}
// Get returns value and boolean that denotes whether value was set.
func (o OptGetUserThumbnailSize) Get() (v GetUserThumbnailSize, ok bool) {
if !o.Set {
return v, false
}
return o.Value, true
}
// Or returns value if set, or given parameter if does not.
func (o OptGetUserThumbnailSize) Or(d GetUserThumbnailSize) GetUserThumbnailSize {
if v, ok := o.Get(); ok {
return v
}
return d
}
// NewOptInt32 returns new OptInt32 with value set to v. // NewOptInt32 returns new OptInt32 with value set to v.
func NewOptInt32(v int32) OptInt32 { func NewOptInt32(v int32) OptInt32 {
return OptInt32{ return OptInt32{
@@ -1302,6 +1999,83 @@ type SetMapfixCompletedNoContent struct{}
// SetSubmissionCompletedNoContent is response for SetSubmissionCompleted operation. // SetSubmissionCompletedNoContent is response for SetSubmissionCompleted operation.
type SetSubmissionCompletedNoContent struct{} type SetSubmissionCompletedNoContent struct{}
// Aggregate statistics for submissions and mapfixes.
// Ref: #/components/schemas/Stats
type Stats struct {
// Total number of submissions.
TotalSubmissions int64 `json:"TotalSubmissions"`
// Total number of mapfixes.
TotalMapfixes int64 `json:"TotalMapfixes"`
// Number of released submissions.
ReleasedSubmissions int64 `json:"ReleasedSubmissions"`
// Number of released mapfixes.
ReleasedMapfixes int64 `json:"ReleasedMapfixes"`
// Number of submissions under review.
SubmittedSubmissions int64 `json:"SubmittedSubmissions"`
// Number of mapfixes under review.
SubmittedMapfixes int64 `json:"SubmittedMapfixes"`
}
// GetTotalSubmissions returns the value of TotalSubmissions.
func (s *Stats) GetTotalSubmissions() int64 {
return s.TotalSubmissions
}
// GetTotalMapfixes returns the value of TotalMapfixes.
func (s *Stats) GetTotalMapfixes() int64 {
return s.TotalMapfixes
}
// GetReleasedSubmissions returns the value of ReleasedSubmissions.
func (s *Stats) GetReleasedSubmissions() int64 {
return s.ReleasedSubmissions
}
// GetReleasedMapfixes returns the value of ReleasedMapfixes.
func (s *Stats) GetReleasedMapfixes() int64 {
return s.ReleasedMapfixes
}
// GetSubmittedSubmissions returns the value of SubmittedSubmissions.
func (s *Stats) GetSubmittedSubmissions() int64 {
return s.SubmittedSubmissions
}
// GetSubmittedMapfixes returns the value of SubmittedMapfixes.
func (s *Stats) GetSubmittedMapfixes() int64 {
return s.SubmittedMapfixes
}
// SetTotalSubmissions sets the value of TotalSubmissions.
func (s *Stats) SetTotalSubmissions(val int64) {
s.TotalSubmissions = val
}
// SetTotalMapfixes sets the value of TotalMapfixes.
func (s *Stats) SetTotalMapfixes(val int64) {
s.TotalMapfixes = val
}
// SetReleasedSubmissions sets the value of ReleasedSubmissions.
func (s *Stats) SetReleasedSubmissions(val int64) {
s.ReleasedSubmissions = val
}
// SetReleasedMapfixes sets the value of ReleasedMapfixes.
func (s *Stats) SetReleasedMapfixes(val int64) {
s.ReleasedMapfixes = val
}
// SetSubmittedSubmissions sets the value of SubmittedSubmissions.
func (s *Stats) SetSubmittedSubmissions(val int64) {
s.SubmittedSubmissions = val
}
// SetSubmittedMapfixes sets the value of SubmittedMapfixes.
func (s *Stats) SetSubmittedMapfixes(val int64) {
s.SubmittedMapfixes = val
}
// Ref: #/components/schemas/Submission // Ref: #/components/schemas/Submission
type Submission struct { type Submission struct {
ID int64 `json:"ID"` ID int64 `json:"ID"`
@@ -1534,6 +2308,23 @@ func (s *Submissions) SetSubmissions(val []Submission) {
s.Submissions = val s.Submissions = val
} }
// UpdateMapfixDescriptionNoContent is response for UpdateMapfixDescription operation.
type UpdateMapfixDescriptionNoContent struct{}
type UpdateMapfixDescriptionReq struct {
Data io.Reader
}
// Read reads data from the Data reader.
//
// Kept to satisfy the io.Reader interface.
func (s UpdateMapfixDescriptionReq) Read(p []byte) (n int, err error) {
if s.Data == nil {
return 0, io.EOF
}
return s.Data.Read(p)
}
// UpdateMapfixModelNoContent is response for UpdateMapfixModel operation. // UpdateMapfixModelNoContent is response for UpdateMapfixModel operation.
type UpdateMapfixModelNoContent struct{} type UpdateMapfixModelNoContent struct{}

View File

@@ -8,7 +8,6 @@ import (
"strings" "strings"
"github.com/go-faster/errors" "github.com/go-faster/errors"
"github.com/ogen-go/ogen/ogenerrors" "github.com/ogen-go/ogen/ogenerrors"
) )
@@ -75,6 +74,7 @@ var operationRolesCookieAuth = map[string][]string{
SessionValidateOperation: []string{}, SessionValidateOperation: []string{},
SetMapfixCompletedOperation: []string{}, SetMapfixCompletedOperation: []string{},
SetSubmissionCompletedOperation: []string{}, SetSubmissionCompletedOperation: []string{},
UpdateMapfixDescriptionOperation: []string{},
UpdateMapfixModelOperation: []string{}, UpdateMapfixModelOperation: []string{},
UpdateScriptOperation: []string{}, UpdateScriptOperation: []string{},
UpdateScriptPolicyOperation: []string{}, UpdateScriptPolicyOperation: []string{},

View File

@@ -155,6 +155,24 @@ type Handler interface {
// //
// POST /submissions/{SubmissionID}/status/reset-uploading // POST /submissions/{SubmissionID}/status/reset-uploading
ActionSubmissionValidated(ctx context.Context, params ActionSubmissionValidatedParams) error ActionSubmissionValidated(ctx context.Context, params ActionSubmissionValidatedParams) error
// BatchAssetThumbnails implements batchAssetThumbnails operation.
//
// Batch fetch asset thumbnails.
//
// POST /thumbnails/assets
BatchAssetThumbnails(ctx context.Context, req *BatchAssetThumbnailsReq) (*BatchAssetThumbnailsOK, error)
// BatchUserThumbnails implements batchUserThumbnails operation.
//
// Batch fetch user avatar thumbnails.
//
// POST /thumbnails/users
BatchUserThumbnails(ctx context.Context, req *BatchUserThumbnailsReq) (*BatchUserThumbnailsOK, error)
// BatchUsernames implements batchUsernames operation.
//
// Batch fetch usernames.
//
// POST /usernames
BatchUsernames(ctx context.Context, req *BatchUsernamesReq) (*BatchUsernamesOK, error)
// CreateMapfix implements createMapfix operation. // CreateMapfix implements createMapfix operation.
// //
// Trigger the validator to create a mapfix. // Trigger the validator to create a mapfix.
@@ -215,6 +233,12 @@ type Handler interface {
// //
// GET /maps/{MapID}/download // GET /maps/{MapID}/download
DownloadMapAsset(ctx context.Context, params DownloadMapAssetParams) (DownloadMapAssetOK, error) DownloadMapAsset(ctx context.Context, params DownloadMapAssetParams) (DownloadMapAssetOK, error)
// GetAssetThumbnail implements getAssetThumbnail operation.
//
// Get single asset thumbnail.
//
// GET /thumbnails/asset/{AssetID}
GetAssetThumbnail(ctx context.Context, params GetAssetThumbnailParams) (*GetAssetThumbnailFound, error)
// GetMap implements getMap operation. // GetMap implements getMap operation.
// //
// Retrieve map with ID. // Retrieve map with ID.
@@ -245,12 +269,24 @@ type Handler interface {
// //
// GET /script-policy/{ScriptPolicyID} // GET /script-policy/{ScriptPolicyID}
GetScriptPolicy(ctx context.Context, params GetScriptPolicyParams) (*ScriptPolicy, error) GetScriptPolicy(ctx context.Context, params GetScriptPolicyParams) (*ScriptPolicy, error)
// GetStats implements getStats operation.
//
// Get aggregate statistics.
//
// GET /stats
GetStats(ctx context.Context) (*Stats, error)
// GetSubmission implements getSubmission operation. // GetSubmission implements getSubmission operation.
// //
// Retrieve map with ID. // Retrieve map with ID.
// //
// GET /submissions/{SubmissionID} // GET /submissions/{SubmissionID}
GetSubmission(ctx context.Context, params GetSubmissionParams) (*Submission, error) GetSubmission(ctx context.Context, params GetSubmissionParams) (*Submission, error)
// GetUserThumbnail implements getUserThumbnail operation.
//
// Get single user avatar thumbnail.
//
// GET /thumbnails/user/{UserID}
GetUserThumbnail(ctx context.Context, params GetUserThumbnailParams) (*GetUserThumbnailFound, error)
// ListMapfixAuditEvents implements listMapfixAuditEvents operation. // ListMapfixAuditEvents implements listMapfixAuditEvents operation.
// //
// Retrieve a list of audit events. // Retrieve a list of audit events.
@@ -329,6 +365,12 @@ type Handler interface {
// //
// POST /submissions/{SubmissionID}/completed // POST /submissions/{SubmissionID}/completed
SetSubmissionCompleted(ctx context.Context, params SetSubmissionCompletedParams) error SetSubmissionCompleted(ctx context.Context, params SetSubmissionCompletedParams) error
// UpdateMapfixDescription implements updateMapfixDescription operation.
//
// Update description (submitter only).
//
// PATCH /mapfixes/{MapfixID}/description
UpdateMapfixDescription(ctx context.Context, req UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error
// UpdateMapfixModel implements updateMapfixModel operation. // UpdateMapfixModel implements updateMapfixModel operation.
// //
// Update model following role restrictions. // Update model following role restrictions.

View File

@@ -232,6 +232,33 @@ func (UnimplementedHandler) ActionSubmissionValidated(ctx context.Context, param
return ht.ErrNotImplemented return ht.ErrNotImplemented
} }
// BatchAssetThumbnails implements batchAssetThumbnails operation.
//
// Batch fetch asset thumbnails.
//
// POST /thumbnails/assets
func (UnimplementedHandler) BatchAssetThumbnails(ctx context.Context, req *BatchAssetThumbnailsReq) (r *BatchAssetThumbnailsOK, _ error) {
return r, ht.ErrNotImplemented
}
// BatchUserThumbnails implements batchUserThumbnails operation.
//
// Batch fetch user avatar thumbnails.
//
// POST /thumbnails/users
func (UnimplementedHandler) BatchUserThumbnails(ctx context.Context, req *BatchUserThumbnailsReq) (r *BatchUserThumbnailsOK, _ error) {
return r, ht.ErrNotImplemented
}
// BatchUsernames implements batchUsernames operation.
//
// Batch fetch usernames.
//
// POST /usernames
func (UnimplementedHandler) BatchUsernames(ctx context.Context, req *BatchUsernamesReq) (r *BatchUsernamesOK, _ error) {
return r, ht.ErrNotImplemented
}
// CreateMapfix implements createMapfix operation. // CreateMapfix implements createMapfix operation.
// //
// Trigger the validator to create a mapfix. // Trigger the validator to create a mapfix.
@@ -322,6 +349,15 @@ func (UnimplementedHandler) DownloadMapAsset(ctx context.Context, params Downloa
return r, ht.ErrNotImplemented return r, ht.ErrNotImplemented
} }
// GetAssetThumbnail implements getAssetThumbnail operation.
//
// Get single asset thumbnail.
//
// GET /thumbnails/asset/{AssetID}
func (UnimplementedHandler) GetAssetThumbnail(ctx context.Context, params GetAssetThumbnailParams) (r *GetAssetThumbnailFound, _ error) {
return r, ht.ErrNotImplemented
}
// GetMap implements getMap operation. // GetMap implements getMap operation.
// //
// Retrieve map with ID. // Retrieve map with ID.
@@ -367,6 +403,15 @@ func (UnimplementedHandler) GetScriptPolicy(ctx context.Context, params GetScrip
return r, ht.ErrNotImplemented return r, ht.ErrNotImplemented
} }
// GetStats implements getStats operation.
//
// Get aggregate statistics.
//
// GET /stats
func (UnimplementedHandler) GetStats(ctx context.Context) (r *Stats, _ error) {
return r, ht.ErrNotImplemented
}
// GetSubmission implements getSubmission operation. // GetSubmission implements getSubmission operation.
// //
// Retrieve map with ID. // Retrieve map with ID.
@@ -376,6 +421,15 @@ func (UnimplementedHandler) GetSubmission(ctx context.Context, params GetSubmiss
return r, ht.ErrNotImplemented return r, ht.ErrNotImplemented
} }
// GetUserThumbnail implements getUserThumbnail operation.
//
// Get single user avatar thumbnail.
//
// GET /thumbnails/user/{UserID}
func (UnimplementedHandler) GetUserThumbnail(ctx context.Context, params GetUserThumbnailParams) (r *GetUserThumbnailFound, _ error) {
return r, ht.ErrNotImplemented
}
// ListMapfixAuditEvents implements listMapfixAuditEvents operation. // ListMapfixAuditEvents implements listMapfixAuditEvents operation.
// //
// Retrieve a list of audit events. // Retrieve a list of audit events.
@@ -493,6 +547,15 @@ func (UnimplementedHandler) SetSubmissionCompleted(ctx context.Context, params S
return ht.ErrNotImplemented return ht.ErrNotImplemented
} }
// UpdateMapfixDescription implements updateMapfixDescription operation.
//
// Update description (submitter only).
//
// PATCH /mapfixes/{MapfixID}/description
func (UnimplementedHandler) UpdateMapfixDescription(ctx context.Context, req UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error {
return ht.ErrNotImplemented
}
// UpdateMapfixModel implements updateMapfixModel operation. // UpdateMapfixModel implements updateMapfixModel operation.
// //
// Update model following role restrictions. // Update model following role restrictions.

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ import (
"git.itzana.me/strafesnet/maps-service/pkg/validator_controller" "git.itzana.me/strafesnet/maps-service/pkg/validator_controller"
"git.itzana.me/strafesnet/maps-service/pkg/web_api" "git.itzana.me/strafesnet/maps-service/pkg/web_api"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/redis/go-redis/v9"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"google.golang.org/grpc" "google.golang.org/grpc"
@@ -102,6 +103,24 @@ func NewServeCommand() *cli.Command {
EnvVars: []string{"RBX_API_KEY"}, EnvVars: []string{"RBX_API_KEY"},
Required: true, Required: true,
}, },
&cli.StringFlag{
Name: "redis-host",
Usage: "Host of Redis cache",
EnvVars: []string{"REDIS_HOST"},
Value: "localhost:6379",
},
&cli.StringFlag{
Name: "redis-password",
Usage: "Password for Redis",
EnvVars: []string{"REDIS_PASSWORD"},
Value: "",
},
&cli.IntFlag{
Name: "redis-db",
Usage: "Redis database number",
EnvVars: []string{"REDIS_DB"},
Value: 0,
},
}, },
} }
} }
@@ -129,6 +148,24 @@ func serve(ctx *cli.Context) error {
log.WithError(err).Fatal("failed to add stream") log.WithError(err).Fatal("failed to add stream")
} }
// Initialize Redis client
redisClient := redis.NewClient(&redis.Options{
Addr: ctx.String("redis-host"),
Password: ctx.String("redis-password"),
DB: ctx.Int("redis-db"),
})
// Test Redis connection
if err := redisClient.Ping(ctx.Context).Err(); err != nil {
log.WithError(err).Warn("failed to connect to Redis - thumbnails will not be cached")
}
// Initialize Roblox client
robloxClient := &roblox.Client{
HttpClient: http.DefaultClient,
ApiKey: ctx.String("rbx-api-key"),
}
// connect to main game database // connect to main game database
conn, err := grpc.Dial(ctx.String("data-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials())) conn, err := grpc.Dial(ctx.String("data-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil { if err != nil {
@@ -139,13 +176,15 @@ func serve(ctx *cli.Context) error {
js, js,
maps.NewMapsServiceClient(conn), maps.NewMapsServiceClient(conn),
users.NewUsersServiceClient(conn), users.NewUsersServiceClient(conn),
robloxClient,
redisClient,
) )
svc_external := web_api.NewService( svc_external := web_api.NewService(
&svc_inner, &svc_inner,
roblox.Client{ roblox.Client{
HttpClient: http.DefaultClient, HttpClient: http.DefaultClient,
ApiKey: ctx.String("rbx-api-key"), ApiKey: ctx.String("rbx-api-key"),
}, },
) )

View File

@@ -17,7 +17,7 @@ type ScriptPolicy struct {
// Hash of the source code that leads to this policy. // Hash of the source code that leads to this policy.
// If this is a replacement mapping, the original source may not be pointed to by any policy. // If this is a replacement mapping, the original source may not be pointed to by any policy.
// The original source should still exist in the scripts table, which can be located by the same hash. // The original source should still exist in the scripts table, which can be located by the same hash.
FromScriptHash int64 // postgres does not support unsigned integers, so we have to pretend FromScriptHash int64 `gorm:"uniqueIndex"` // postgres does not support unsigned integers, so we have to pretend
// The ID of the replacement source (ScriptPolicyReplace) // The ID of the replacement source (ScriptPolicyReplace)
// or verbatim source (ScriptPolicyAllowed) // or verbatim source (ScriptPolicyAllowed)
// or 0 (other) // or 0 (other)

View File

@@ -26,7 +26,7 @@ func HashParse(hash string) (uint64, error){
type Script struct { type Script struct {
ID int64 `gorm:"primaryKey"` ID int64 `gorm:"primaryKey"`
Name string Name string
Hash int64 // postgres does not support unsigned integers, so we have to pretend Hash int64 `gorm:"uniqueIndex"` // postgres does not support unsigned integers, so we have to pretend
Source string Source string
ResourceType ResourceType // is this a submission or is it a mapfix ResourceType ResourceType // is this a submission or is it a mapfix
ResourceID int64 // which submission / mapfix did this script first appear in ResourceID int64 // which submission / mapfix did this script first appear in

160
pkg/roblox/thumbnails.go Normal file
View File

@@ -0,0 +1,160 @@
package roblox
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// ThumbnailSize represents valid Roblox thumbnail sizes
type ThumbnailSize string
const (
Size150x150 ThumbnailSize = "150x150"
Size420x420 ThumbnailSize = "420x420"
Size768x432 ThumbnailSize = "768x432"
)
// ThumbnailFormat represents the image format
type ThumbnailFormat string
const (
FormatPng ThumbnailFormat = "Png"
FormatJpeg ThumbnailFormat = "Jpeg"
)
// ThumbnailRequest represents a single thumbnail request
type ThumbnailRequest struct {
RequestID string `json:"requestId,omitempty"`
Type string `json:"type"`
TargetID uint64 `json:"targetId"`
Size string `json:"size,omitempty"`
Format string `json:"format,omitempty"`
}
// ThumbnailData represents a single thumbnail response
type ThumbnailData struct {
TargetID uint64 `json:"targetId"`
State string `json:"state"` // "Completed", "Error", "Pending"
ImageURL string `json:"imageUrl"`
}
// BatchThumbnailsResponse represents the API response
type BatchThumbnailsResponse struct {
Data []ThumbnailData `json:"data"`
}
// GetAssetThumbnails fetches thumbnails for multiple assets in a single batch request
// Roblox allows up to 100 assets per batch
func (c *Client) GetAssetThumbnails(assetIDs []uint64, size ThumbnailSize, format ThumbnailFormat) ([]ThumbnailData, error) {
if len(assetIDs) == 0 {
return []ThumbnailData{}, nil
}
if len(assetIDs) > 100 {
return nil, GetError("batch size cannot exceed 100 assets")
}
// Build request payload - the API expects an array directly, not wrapped in an object
requests := make([]ThumbnailRequest, len(assetIDs))
for i, assetID := range assetIDs {
requests[i] = ThumbnailRequest{
Type: "Asset",
TargetID: assetID,
Size: string(size),
Format: string(format),
}
}
jsonData, err := json.Marshal(requests)
if err != nil {
return nil, GetError("JSONMarshalError: " + err.Error())
}
req, err := http.NewRequest("POST", "https://thumbnails.roblox.com/v1/batch", bytes.NewBuffer(jsonData))
if err != nil {
return nil, GetError("RequestCreationError: " + err.Error())
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, GetError("RequestError: " + err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, GetError("ReadBodyError: " + err.Error())
}
var response BatchThumbnailsResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, GetError("JSONUnmarshalError: " + err.Error())
}
return response.Data, nil
}
// GetUserAvatarThumbnails fetches avatar thumbnails for multiple users in a single batch request
func (c *Client) GetUserAvatarThumbnails(userIDs []uint64, size ThumbnailSize, format ThumbnailFormat) ([]ThumbnailData, error) {
if len(userIDs) == 0 {
return []ThumbnailData{}, nil
}
if len(userIDs) > 100 {
return nil, GetError("batch size cannot exceed 100 users")
}
// Build request payload - the API expects an array directly, not wrapped in an object
requests := make([]ThumbnailRequest, len(userIDs))
for i, userID := range userIDs {
requests[i] = ThumbnailRequest{
Type: "AvatarHeadShot",
TargetID: userID,
Size: string(size),
Format: string(format),
}
}
jsonData, err := json.Marshal(requests)
if err != nil {
return nil, GetError("JSONMarshalError: " + err.Error())
}
req, err := http.NewRequest("POST", "https://thumbnails.roblox.com/v1/batch", bytes.NewBuffer(jsonData))
if err != nil {
return nil, GetError("RequestCreationError: " + err.Error())
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, GetError("RequestError: " + err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, GetError("ReadBodyError: " + err.Error())
}
var response BatchThumbnailsResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, GetError("JSONUnmarshalError: " + err.Error())
}
return response.Data, nil
}

72
pkg/roblox/users.go Normal file
View File

@@ -0,0 +1,72 @@
package roblox
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// UserData represents a single user's information
type UserData struct {
ID uint64 `json:"id"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
}
// BatchUsersResponse represents the API response for batch user requests
type BatchUsersResponse struct {
Data []UserData `json:"data"`
}
// GetUsernames fetches usernames for multiple users in a single batch request
// Roblox allows up to 100 users per batch
func (c *Client) GetUsernames(userIDs []uint64) ([]UserData, error) {
if len(userIDs) == 0 {
return []UserData{}, nil
}
if len(userIDs) > 100 {
return nil, GetError("batch size cannot exceed 100 users")
}
// Build request payload
payload := map[string][]uint64{
"userIds": userIDs,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, GetError("JSONMarshalError: " + err.Error())
}
req, err := http.NewRequest("POST", "https://users.roblox.com/v1/users", bytes.NewBuffer(jsonData))
if err != nil {
return nil, GetError("RequestCreationError: " + err.Error())
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, GetError("RequestError: " + err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, GetError("ReadBodyError: " + err.Error())
}
var response BatchUsersResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, GetError("JSONUnmarshalError: " + err.Error())
}
return response.Data, nil
}

View File

@@ -1,17 +1,22 @@
package service package service
import ( import (
"context"
"git.itzana.me/strafesnet/go-grpc/maps" "git.itzana.me/strafesnet/go-grpc/maps"
"git.itzana.me/strafesnet/go-grpc/users" "git.itzana.me/strafesnet/go-grpc/users"
"git.itzana.me/strafesnet/maps-service/pkg/datastore" "git.itzana.me/strafesnet/maps-service/pkg/datastore"
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/redis/go-redis/v9"
) )
type Service struct { type Service struct {
db datastore.Datastore db datastore.Datastore
nats nats.JetStreamContext nats nats.JetStreamContext
maps maps.MapsServiceClient maps maps.MapsServiceClient
users users.UsersServiceClient users users.UsersServiceClient
thumbnailService *ThumbnailService
} }
func NewService( func NewService(
@@ -19,11 +24,44 @@ func NewService(
nats nats.JetStreamContext, nats nats.JetStreamContext,
maps maps.MapsServiceClient, maps maps.MapsServiceClient,
users users.UsersServiceClient, users users.UsersServiceClient,
robloxClient *roblox.Client,
redisClient *redis.Client,
) Service { ) Service {
return Service{ return Service{
db: db, db: db,
nats: nats, nats: nats,
maps: maps, maps: maps,
users: users, users: users,
thumbnailService: NewThumbnailService(robloxClient, redisClient),
} }
} }
// GetAssetThumbnails proxies to the thumbnail service
func (s *Service) GetAssetThumbnails(ctx context.Context, assetIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
return s.thumbnailService.GetAssetThumbnails(ctx, assetIDs, size)
}
// GetUserAvatarThumbnails proxies to the thumbnail service
func (s *Service) GetUserAvatarThumbnails(ctx context.Context, userIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
return s.thumbnailService.GetUserAvatarThumbnails(ctx, userIDs, size)
}
// GetSingleAssetThumbnail proxies to the thumbnail service
func (s *Service) GetSingleAssetThumbnail(ctx context.Context, assetID uint64, size roblox.ThumbnailSize) (string, error) {
return s.thumbnailService.GetSingleAssetThumbnail(ctx, assetID, size)
}
// GetSingleUserAvatarThumbnail proxies to the thumbnail service
func (s *Service) GetSingleUserAvatarThumbnail(ctx context.Context, userID uint64, size roblox.ThumbnailSize) (string, error) {
return s.thumbnailService.GetSingleUserAvatarThumbnail(ctx, userID, size)
}
// GetUsernames proxies to the thumbnail service
func (s *Service) GetUsernames(ctx context.Context, userIDs []uint64) (map[uint64]string, error) {
return s.thumbnailService.GetUsernames(ctx, userIDs)
}
// GetSingleUsername proxies to the thumbnail service
func (s *Service) GetSingleUsername(ctx context.Context, userID uint64) (string, error) {
return s.thumbnailService.GetSingleUsername(ctx, userID)
}

218
pkg/service/thumbnails.go Normal file
View File

@@ -0,0 +1,218 @@
package service
import (
"context"
"encoding/json"
"fmt"
"time"
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
"github.com/redis/go-redis/v9"
)
type ThumbnailService struct {
robloxClient *roblox.Client
redisClient *redis.Client
cacheTTL time.Duration
}
func NewThumbnailService(robloxClient *roblox.Client, redisClient *redis.Client) *ThumbnailService {
return &ThumbnailService{
robloxClient: robloxClient,
redisClient: redisClient,
cacheTTL: 24 * time.Hour, // Cache thumbnails for 24 hours
}
}
// CachedThumbnail represents a cached thumbnail entry
type CachedThumbnail struct {
ImageURL string `json:"imageUrl"`
State string `json:"state"`
CachedAt time.Time `json:"cachedAt"`
}
// GetAssetThumbnails fetches thumbnails with Redis caching and batching
func (s *ThumbnailService) GetAssetThumbnails(ctx context.Context, assetIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
if len(assetIDs) == 0 {
return map[uint64]string{}, nil
}
result := make(map[uint64]string)
var missingIDs []uint64
// Try to get from cache first
for _, assetID := range assetIDs {
cacheKey := fmt.Sprintf("thumbnail:asset:%d:%s", assetID, size)
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
if err == redis.Nil {
// Cache miss
missingIDs = append(missingIDs, assetID)
} else if err != nil {
// Redis error - treat as cache miss
missingIDs = append(missingIDs, assetID)
} else {
// Cache hit
var thumbnail CachedThumbnail
if err := json.Unmarshal([]byte(cached), &thumbnail); err == nil && thumbnail.State == "Completed" {
result[assetID] = thumbnail.ImageURL
} else {
missingIDs = append(missingIDs, assetID)
}
}
}
// If all were cached, return early
if len(missingIDs) == 0 {
return result, nil
}
// Batch fetch missing thumbnails from Roblox API
// Split into batches of 100 (Roblox API limit)
for i := 0; i < len(missingIDs); i += 100 {
end := i + 100
if end > len(missingIDs) {
end = len(missingIDs)
}
batch := missingIDs[i:end]
thumbnails, err := s.robloxClient.GetAssetThumbnails(batch, size, roblox.FormatPng)
if err != nil {
return nil, fmt.Errorf("failed to fetch thumbnails: %w", err)
}
// Process results and cache them
for _, thumb := range thumbnails {
cached := CachedThumbnail{
ImageURL: thumb.ImageURL,
State: thumb.State,
CachedAt: time.Now(),
}
if thumb.State == "Completed" && thumb.ImageURL != "" {
result[thumb.TargetID] = thumb.ImageURL
}
// Cache the result (even if incomplete, to avoid repeated API calls)
cacheKey := fmt.Sprintf("thumbnail:asset:%d:%s", thumb.TargetID, size)
cachedJSON, _ := json.Marshal(cached)
// Use shorter TTL for incomplete thumbnails
ttl := s.cacheTTL
if thumb.State != "Completed" {
ttl = 5 * time.Minute
}
s.redisClient.Set(ctx, cacheKey, cachedJSON, ttl)
}
}
return result, nil
}
// GetUserAvatarThumbnails fetches user avatar thumbnails with Redis caching and batching
func (s *ThumbnailService) GetUserAvatarThumbnails(ctx context.Context, userIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
if len(userIDs) == 0 {
return map[uint64]string{}, nil
}
result := make(map[uint64]string)
var missingIDs []uint64
// Try to get from cache first
for _, userID := range userIDs {
cacheKey := fmt.Sprintf("thumbnail:user:%d:%s", userID, size)
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
if err == redis.Nil {
// Cache miss
missingIDs = append(missingIDs, userID)
} else if err != nil {
// Redis error - treat as cache miss
missingIDs = append(missingIDs, userID)
} else {
// Cache hit
var thumbnail CachedThumbnail
if err := json.Unmarshal([]byte(cached), &thumbnail); err == nil && thumbnail.State == "Completed" {
result[userID] = thumbnail.ImageURL
} else {
missingIDs = append(missingIDs, userID)
}
}
}
// If all were cached, return early
if len(missingIDs) == 0 {
return result, nil
}
// Batch fetch missing thumbnails from Roblox API
// Split into batches of 100 (Roblox API limit)
for i := 0; i < len(missingIDs); i += 100 {
end := i + 100
if end > len(missingIDs) {
end = len(missingIDs)
}
batch := missingIDs[i:end]
thumbnails, err := s.robloxClient.GetUserAvatarThumbnails(batch, size, roblox.FormatPng)
if err != nil {
return nil, fmt.Errorf("failed to fetch user thumbnails: %w", err)
}
// Process results and cache them
for _, thumb := range thumbnails {
cached := CachedThumbnail{
ImageURL: thumb.ImageURL,
State: thumb.State,
CachedAt: time.Now(),
}
if thumb.State == "Completed" && thumb.ImageURL != "" {
result[thumb.TargetID] = thumb.ImageURL
}
// Cache the result
cacheKey := fmt.Sprintf("thumbnail:user:%d:%s", thumb.TargetID, size)
cachedJSON, _ := json.Marshal(cached)
// Use shorter TTL for incomplete thumbnails
ttl := s.cacheTTL
if thumb.State != "Completed" {
ttl = 5 * time.Minute
}
s.redisClient.Set(ctx, cacheKey, cachedJSON, ttl)
}
}
return result, nil
}
// GetSingleAssetThumbnail is a convenience method for fetching a single asset thumbnail
func (s *ThumbnailService) GetSingleAssetThumbnail(ctx context.Context, assetID uint64, size roblox.ThumbnailSize) (string, error) {
results, err := s.GetAssetThumbnails(ctx, []uint64{assetID}, size)
if err != nil {
return "", err
}
if url, ok := results[assetID]; ok {
return url, nil
}
return "", fmt.Errorf("thumbnail not available for asset %d", assetID)
}
// GetSingleUserAvatarThumbnail is a convenience method for fetching a single user avatar thumbnail
func (s *ThumbnailService) GetSingleUserAvatarThumbnail(ctx context.Context, userID uint64, size roblox.ThumbnailSize) (string, error) {
results, err := s.GetUserAvatarThumbnails(ctx, []uint64{userID}, size)
if err != nil {
return "", err
}
if url, ok := results[userID]; ok {
return url, nil
}
return "", fmt.Errorf("thumbnail not available for user %d", userID)
}

108
pkg/service/users.go Normal file
View File

@@ -0,0 +1,108 @@
package service
import (
"context"
"encoding/json"
"fmt"
"time"
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
"github.com/redis/go-redis/v9"
)
// CachedUser represents a cached user entry
type CachedUser struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
CachedAt time.Time `json:"cachedAt"`
}
// GetUsernames fetches usernames with Redis caching and batching
func (s *ThumbnailService) GetUsernames(ctx context.Context, userIDs []uint64) (map[uint64]string, error) {
if len(userIDs) == 0 {
return map[uint64]string{}, nil
}
result := make(map[uint64]string)
var missingIDs []uint64
// Try to get from cache first
for _, userID := range userIDs {
cacheKey := fmt.Sprintf("user:name:%d", userID)
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
if err == redis.Nil {
// Cache miss
missingIDs = append(missingIDs, userID)
} else if err != nil {
// Redis error - treat as cache miss
missingIDs = append(missingIDs, userID)
} else {
// Cache hit
var user CachedUser
if err := json.Unmarshal([]byte(cached), &user); err == nil && user.Name != "" {
result[userID] = user.Name
} else {
missingIDs = append(missingIDs, userID)
}
}
}
// If all were cached, return early
if len(missingIDs) == 0 {
return result, nil
}
// Batch fetch missing usernames from Roblox API
// Split into batches of 100 (Roblox API limit)
for i := 0; i < len(missingIDs); i += 100 {
end := i + 100
if end > len(missingIDs) {
end = len(missingIDs)
}
batch := missingIDs[i:end]
var users []roblox.UserData
var err error
users, err = s.robloxClient.GetUsernames(batch)
if err != nil {
return nil, fmt.Errorf("failed to fetch usernames: %w", err)
}
// Process results and cache them
for _, user := range users {
cached := CachedUser{
Name: user.Name,
DisplayName: user.DisplayName,
CachedAt: time.Now(),
}
if user.Name != "" {
result[user.ID] = user.Name
}
// Cache the result
cacheKey := fmt.Sprintf("user:name:%d", user.ID)
cachedJSON, _ := json.Marshal(cached)
// Cache usernames for a long time (7 days) since they rarely change
s.redisClient.Set(ctx, cacheKey, cachedJSON, 7*24*time.Hour)
}
}
return result, nil
}
// GetSingleUsername is a convenience method for fetching a single username
func (s *ThumbnailService) GetSingleUsername(ctx context.Context, userID uint64) (string, error) {
results, err := s.GetUsernames(ctx, []uint64{userID})
if err != nil {
return "", err
}
if name, ok := results[userID]; ok {
return name, nil
}
return "", fmt.Errorf("username not available for user %d", userID)
}

View File

@@ -327,6 +327,48 @@ func (svc *Service) UpdateMapfixModel(ctx context.Context, params api.UpdateMapf
) )
} }
// UpdateMapfixDescription implements updateMapfixDescription operation.
//
// Update description (submitter only, status ChangesRequested or UnderConstruction).
//
// PATCH /mapfixes/{MapfixID}/description
func (svc *Service) UpdateMapfixDescription(ctx context.Context, req api.UpdateMapfixDescriptionReq, params api.UpdateMapfixDescriptionParams) error {
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
if !ok {
return ErrUserInfo
}
// read mapfix
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
if err != nil {
return err
}
userId, err := userInfo.GetUserID()
if err != nil {
return err
}
// check if caller is the submitter
if userId != mapfix.Submitter {
return ErrPermissionDeniedNotSubmitter
}
// read the new description from request body
data, err := io.ReadAll(req)
if err != nil {
return err
}
newDescription := string(data)
// check if Status is ChangesRequested or UnderConstruction
update := service.NewMapfixUpdate()
update.SetDescription(newDescription)
allow_statuses := []model.MapfixStatus{model.MapfixStatusChangesRequested, model.MapfixStatusUnderConstruction}
return svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
}
// ActionMapfixReject invokes actionMapfixReject operation. // ActionMapfixReject invokes actionMapfixReject operation.
// //
// Role Reviewer changes status from Submitted -> Rejected. // Role Reviewer changes status from Submitted -> Rejected.

View File

@@ -36,10 +36,28 @@ func (svc *Service) CreateScript(ctx context.Context, req *api.ScriptCreate) (*a
return nil, err return nil, err
} }
hash := int64(model.HashSource(req.Source))
// Check if a script with this hash already exists
filter := service.NewScriptFilter()
filter.SetHash(hash)
existingScripts, err := svc.inner.ListScripts(ctx, filter, model.Page{Number: 1, Size: 1})
if err != nil {
return nil, err
}
// If script with this hash exists, return existing script ID
if len(existingScripts) > 0 {
return &api.ScriptID{
ScriptID: existingScripts[0].ID,
}, nil
}
// Create new script
script, err := svc.inner.CreateScript(ctx, model.Script{ script, err := svc.inner.CreateScript(ctx, model.Script{
ID: 0, ID: 0,
Name: req.Name, Name: req.Name,
Hash: int64(model.HashSource(req.Source)), Hash: hash,
Source: req.Source, Source: req.Source,
ResourceType: model.ResourceType(req.ResourceType), ResourceType: model.ResourceType(req.ResourceType),
ResourceID: req.ResourceID.Or(0), ResourceID: req.ResourceID.Or(0),

105
pkg/web_api/stats.go Normal file
View File

@@ -0,0 +1,105 @@
package web_api
import (
"context"
"git.itzana.me/strafesnet/maps-service/pkg/api"
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
"git.itzana.me/strafesnet/maps-service/pkg/model"
"git.itzana.me/strafesnet/maps-service/pkg/service"
)
// GET /stats
func (svc *Service) GetStats(ctx context.Context) (*api.Stats, error) {
// Get total submissions count
totalSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, service.NewSubmissionFilter(), model.Page{
Number: 1,
Size: 0, // We only want the count, not the items
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
// Get total mapfixes count
totalMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, service.NewMapfixFilter(), model.Page{
Number: 1,
Size: 0, // We only want the count, not the items
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
// Get released submissions count
releasedSubmissionsFilter := service.NewSubmissionFilter()
releasedSubmissionsFilter.SetStatuses([]model.SubmissionStatus{model.SubmissionStatusReleased})
releasedSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, releasedSubmissionsFilter, model.Page{
Number: 1,
Size: 0,
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
// Get released mapfixes count
releasedMapfixesFilter := service.NewMapfixFilter()
releasedMapfixesFilter.SetStatuses([]model.MapfixStatus{model.MapfixStatusReleased})
releasedMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, releasedMapfixesFilter, model.Page{
Number: 1,
Size: 0,
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
// Get submitted submissions count (under review)
submittedSubmissionsFilter := service.NewSubmissionFilter()
submittedSubmissionsFilter.SetStatuses([]model.SubmissionStatus{
model.SubmissionStatusUnderConstruction,
model.SubmissionStatusChangesRequested,
model.SubmissionStatusSubmitting,
model.SubmissionStatusSubmitted,
model.SubmissionStatusAcceptedUnvalidated,
model.SubmissionStatusValidating,
model.SubmissionStatusValidated,
model.SubmissionStatusUploading,
model.SubmissionStatusUploaded,
})
submittedSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, submittedSubmissionsFilter, model.Page{
Number: 1,
Size: 0,
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
// Get submitted mapfixes count (under review)
submittedMapfixesFilter := service.NewMapfixFilter()
submittedMapfixesFilter.SetStatuses([]model.MapfixStatus{
model.MapfixStatusUnderConstruction,
model.MapfixStatusChangesRequested,
model.MapfixStatusSubmitting,
model.MapfixStatusSubmitted,
model.MapfixStatusAcceptedUnvalidated,
model.MapfixStatusValidating,
model.MapfixStatusValidated,
model.MapfixStatusUploading,
model.MapfixStatusUploaded,
model.MapfixStatusReleasing,
})
submittedMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, submittedMapfixesFilter, model.Page{
Number: 1,
Size: 0,
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
return &api.Stats{
TotalSubmissions: totalSubmissions,
TotalMapfixes: totalMapfixes,
ReleasedSubmissions: releasedSubmissions,
ReleasedMapfixes: releasedMapfixes,
SubmittedSubmissions: submittedSubmissions,
SubmittedMapfixes: submittedMapfixes,
}, nil
}

135
pkg/web_api/thumbnails.go Normal file
View File

@@ -0,0 +1,135 @@
package web_api
import (
"context"
"strconv"
"git.itzana.me/strafesnet/maps-service/pkg/api"
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
)
// BatchAssetThumbnails handles batch fetching of asset thumbnails
func (svc *Service) BatchAssetThumbnails(ctx context.Context, req *api.BatchAssetThumbnailsReq) (*api.BatchAssetThumbnailsOK, error) {
if len(req.AssetIds) == 0 {
return &api.BatchAssetThumbnailsOK{
Thumbnails: api.NewOptBatchAssetThumbnailsOKThumbnails(map[string]string{}),
}, nil
}
// Convert size string to enum
size := roblox.Size420x420
if req.Size.IsSet() {
sizeStr := req.Size.Value
switch api.BatchAssetThumbnailsReqSize(sizeStr) {
case api.BatchAssetThumbnailsReqSize150x150:
size = roblox.Size150x150
case api.BatchAssetThumbnailsReqSize768x432:
size = roblox.Size768x432
}
}
// Fetch thumbnails from service
thumbnails, err := svc.inner.GetAssetThumbnails(ctx, req.AssetIds, size)
if err != nil {
return nil, err
}
// Convert map[uint64]string to map[string]string for JSON
result := make(map[string]string, len(thumbnails))
for assetID, url := range thumbnails {
result[strconv.FormatUint(assetID, 10)] = url
}
return &api.BatchAssetThumbnailsOK{
Thumbnails: api.NewOptBatchAssetThumbnailsOKThumbnails(result),
}, nil
}
// GetAssetThumbnail handles single asset thumbnail fetch (with redirect)
func (svc *Service) GetAssetThumbnail(ctx context.Context, params api.GetAssetThumbnailParams) (*api.GetAssetThumbnailFound, error) {
// Convert size string to enum
size := roblox.Size420x420
if params.Size.IsSet() {
sizeStr := params.Size.Value
switch api.GetAssetThumbnailSize(sizeStr) {
case api.GetAssetThumbnailSize150x150:
size = roblox.Size150x150
case api.GetAssetThumbnailSize768x432:
size = roblox.Size768x432
}
}
// Fetch thumbnail
thumbnailURL, err := svc.inner.GetSingleAssetThumbnail(ctx, params.AssetID, size)
if err != nil {
return nil, err
}
// Return redirect response
return &api.GetAssetThumbnailFound{
Location: api.NewOptString(thumbnailURL),
}, nil
}
// BatchUserThumbnails handles batch fetching of user avatar thumbnails
func (svc *Service) BatchUserThumbnails(ctx context.Context, req *api.BatchUserThumbnailsReq) (*api.BatchUserThumbnailsOK, error) {
if len(req.UserIds) == 0 {
return &api.BatchUserThumbnailsOK{
Thumbnails: api.NewOptBatchUserThumbnailsOKThumbnails(map[string]string{}),
}, nil
}
// Convert size string to enum
size := roblox.Size150x150
if req.Size.IsSet() {
sizeStr := req.Size.Value
switch api.BatchUserThumbnailsReqSize(sizeStr) {
case api.BatchUserThumbnailsReqSize420x420:
size = roblox.Size420x420
case api.BatchUserThumbnailsReqSize768x432:
size = roblox.Size768x432
}
}
// Fetch thumbnails from service
thumbnails, err := svc.inner.GetUserAvatarThumbnails(ctx, req.UserIds, size)
if err != nil {
return nil, err
}
// Convert map[uint64]string to map[string]string for JSON
result := make(map[string]string, len(thumbnails))
for userID, url := range thumbnails {
result[strconv.FormatUint(userID, 10)] = url
}
return &api.BatchUserThumbnailsOK{
Thumbnails: api.NewOptBatchUserThumbnailsOKThumbnails(result),
}, nil
}
// GetUserThumbnail handles single user avatar thumbnail fetch (with redirect)
func (svc *Service) GetUserThumbnail(ctx context.Context, params api.GetUserThumbnailParams) (*api.GetUserThumbnailFound, error) {
// Convert size string to enum
size := roblox.Size150x150
if params.Size.IsSet() {
sizeStr := params.Size.Value
switch api.GetUserThumbnailSize(sizeStr) {
case api.GetUserThumbnailSize420x420:
size = roblox.Size420x420
case api.GetUserThumbnailSize768x432:
size = roblox.Size768x432
}
}
// Fetch thumbnail
thumbnailURL, err := svc.inner.GetSingleUserAvatarThumbnail(ctx, params.UserID, size)
if err != nil {
return nil, err
}
// Return redirect response
return &api.GetUserThumbnailFound{
Location: api.NewOptString(thumbnailURL),
}, nil
}

33
pkg/web_api/users.go Normal file
View File

@@ -0,0 +1,33 @@
package web_api
import (
"context"
"strconv"
"git.itzana.me/strafesnet/maps-service/pkg/api"
)
// BatchUsernames handles batch fetching of usernames
func (svc *Service) BatchUsernames(ctx context.Context, req *api.BatchUsernamesReq) (*api.BatchUsernamesOK, error) {
if len(req.UserIds) == 0 {
return &api.BatchUsernamesOK{
Usernames: api.NewOptBatchUsernamesOKUsernames(map[string]string{}),
}, nil
}
// Fetch usernames from service
usernames, err := svc.inner.GetUsernames(ctx, req.UserIds)
if err != nil {
return nil, err
}
// Convert map[uint64]string to map[string]string for JSON
result := make(map[string]string, len(usernames))
for userID, username := range usernames {
result[strconv.FormatUint(userID, 10)] = username
}
return &api.BatchUsernamesOK{
Usernames: api.NewOptBatchUsernamesOKUsernames(result),
}, nil
}

View File

@@ -30,7 +30,6 @@ impl<Items> std::error::Error for SingleItemError<Items> where Items:std::fmt::D
pub type ScriptSingleItemError=SingleItemError<Vec<ScriptID>>; pub type ScriptSingleItemError=SingleItemError<Vec<ScriptID>>;
pub type ScriptPolicySingleItemError=SingleItemError<Vec<ScriptPolicyID>>; pub type ScriptPolicySingleItemError=SingleItemError<Vec<ScriptPolicyID>>;
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub struct UrlAndBody{ pub struct UrlAndBody{
pub url:url::Url, pub url:url::Url,
@@ -76,7 +75,7 @@ pub enum GameID{
FlyTrials=5, FlyTrials=5,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)] #[derive(Clone,Debug,serde::Serialize)]
pub struct CreateMapfixRequest<'a>{ pub struct CreateMapfixRequest<'a>{
pub OperationID:OperationID, pub OperationID:OperationID,
@@ -89,13 +88,13 @@ pub struct CreateMapfixRequest<'a>{
pub TargetAssetID:u64, pub TargetAssetID:u64,
pub Description:&'a str, pub Description:&'a str,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct MapfixIDResponse{ pub struct MapfixIDResponse{
pub MapfixID:MapfixID, pub MapfixID:MapfixID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)] #[derive(Clone,Debug,serde::Serialize)]
pub struct CreateSubmissionRequest<'a>{ pub struct CreateSubmissionRequest<'a>{
pub OperationID:OperationID, pub OperationID:OperationID,
@@ -108,7 +107,7 @@ pub struct CreateSubmissionRequest<'a>{
pub Status:u32, pub Status:u32,
pub Roles:u32, pub Roles:u32,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct SubmissionIDResponse{ pub struct SubmissionIDResponse{
pub SubmissionID:SubmissionID, pub SubmissionID:SubmissionID,
@@ -127,11 +126,11 @@ pub enum ResourceType{
Submission=2, Submission=2,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
pub struct GetScriptRequest{ pub struct GetScriptRequest{
pub ScriptID:ScriptID, pub ScriptID:ScriptID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)] #[derive(Clone,Debug,serde::Serialize)]
pub struct GetScriptsRequest<'a>{ pub struct GetScriptsRequest<'a>{
pub Page:u32, pub Page:u32,
@@ -151,7 +150,7 @@ pub struct GetScriptsRequest<'a>{
pub struct HashRequest<'a>{ pub struct HashRequest<'a>{
pub hash:&'a str, pub hash:&'a str,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct ScriptResponse{ pub struct ScriptResponse{
pub ID:ScriptID, pub ID:ScriptID,
@@ -161,7 +160,7 @@ pub struct ScriptResponse{
pub ResourceType:ResourceType, pub ResourceType:ResourceType,
pub ResourceID:ResourceID, pub ResourceID:ResourceID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)] #[derive(Clone,Debug,serde::Serialize)]
pub struct CreateScriptRequest<'a>{ pub struct CreateScriptRequest<'a>{
pub Name:&'a str, pub Name:&'a str,
@@ -170,7 +169,7 @@ pub struct CreateScriptRequest<'a>{
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if="Option::is_none")]
pub ResourceID:Option<ResourceID>, pub ResourceID:Option<ResourceID>,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct ScriptIDResponse{ pub struct ScriptIDResponse{
pub ScriptID:ScriptID, pub ScriptID:ScriptID,
@@ -186,11 +185,11 @@ pub enum Policy{
Replace=4, Replace=4,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
pub struct GetScriptPolicyRequest{ pub struct GetScriptPolicyRequest{
pub ScriptPolicyID:ScriptPolicyID, pub ScriptPolicyID:ScriptPolicyID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)] #[derive(Clone,Debug,serde::Serialize)]
pub struct GetScriptPoliciesRequest<'a>{ pub struct GetScriptPoliciesRequest<'a>{
pub Page:u32, pub Page:u32,
@@ -202,7 +201,7 @@ pub struct GetScriptPoliciesRequest<'a>{
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if="Option::is_none")]
pub Policy:Option<Policy>, pub Policy:Option<Policy>,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct ScriptPolicyResponse{ pub struct ScriptPolicyResponse{
pub ID:ScriptPolicyID, pub ID:ScriptPolicyID,
@@ -210,20 +209,20 @@ pub struct ScriptPolicyResponse{
pub ToScriptID:ScriptID, pub ToScriptID:ScriptID,
pub Policy:Policy pub Policy:Policy
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)] #[derive(Clone,Debug,serde::Serialize)]
pub struct CreateScriptPolicyRequest{ pub struct CreateScriptPolicyRequest{
pub FromScriptID:ScriptID, pub FromScriptID:ScriptID,
pub ToScriptID:ScriptID, pub ToScriptID:ScriptID,
pub Policy:Policy, pub Policy:Policy,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct ScriptPolicyIDResponse{ pub struct ScriptPolicyIDResponse{
pub ScriptPolicyID:ScriptPolicyID, pub ScriptPolicyID:ScriptPolicyID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)] #[derive(Clone,Debug,serde::Serialize)]
pub struct UpdateScriptPolicyRequest{ pub struct UpdateScriptPolicyRequest{
pub ID:ScriptPolicyID, pub ID:ScriptPolicyID,
@@ -235,7 +234,7 @@ pub struct UpdateScriptPolicyRequest{
pub Policy:Option<Policy>, pub Policy:Option<Policy>,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct UpdateSubmissionModelRequest{ pub struct UpdateSubmissionModelRequest{
pub SubmissionID:SubmissionID, pub SubmissionID:SubmissionID,
@@ -276,7 +275,7 @@ pub enum MapfixStatus{
Released=10, Released=10,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct GetMapfixesRequest<'a>{ pub struct GetMapfixesRequest<'a>{
pub Page:u32, pub Page:u32,
@@ -292,7 +291,7 @@ pub struct GetMapfixesRequest<'a>{
pub StatusID:Option<MapfixStatus>, pub StatusID:Option<MapfixStatus>,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize,serde::Deserialize)] #[derive(Clone,Debug,serde::Serialize,serde::Deserialize)]
pub struct MapfixResponse{ pub struct MapfixResponse{
pub ID:MapfixID, pub ID:MapfixID,
@@ -312,7 +311,7 @@ pub struct MapfixResponse{
pub Description:String, pub Description:String,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct MapfixesResponse{ pub struct MapfixesResponse{
pub Total:u64, pub Total:u64,
@@ -342,7 +341,7 @@ pub enum SubmissionStatus{
Released=10, Released=10,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct GetSubmissionsRequest<'a>{ pub struct GetSubmissionsRequest<'a>{
pub Page:u32, pub Page:u32,
@@ -358,7 +357,7 @@ pub struct GetSubmissionsRequest<'a>{
pub StatusID:Option<SubmissionStatus>, pub StatusID:Option<SubmissionStatus>,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct SubmissionResponse{ pub struct SubmissionResponse{
pub ID:SubmissionID, pub ID:SubmissionID,
@@ -376,14 +375,14 @@ pub struct SubmissionResponse{
pub StatusID:SubmissionStatus, pub StatusID:SubmissionStatus,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct SubmissionsResponse{ pub struct SubmissionsResponse{
pub Total:u64, pub Total:u64,
pub Submissions:Vec<SubmissionResponse>, pub Submissions:Vec<SubmissionResponse>,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct GetMapsRequest<'a>{ pub struct GetMapsRequest<'a>{
pub Page:u32, pub Page:u32,
@@ -394,7 +393,7 @@ pub struct GetMapsRequest<'a>{
pub GameID:Option<GameID>, pub GameID:Option<GameID>,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct MapResponse{ pub struct MapResponse{
pub ID:i64, pub ID:i64,
@@ -404,7 +403,7 @@ pub struct MapResponse{
pub Date:i64, pub Date:i64,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct GetMapfixAuditEventsRequest{ pub struct GetMapfixAuditEventsRequest{
pub Page:u32, pub Page:u32,
@@ -412,7 +411,7 @@ pub struct GetMapfixAuditEventsRequest{
pub MapfixID:i64, pub MapfixID:i64,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct GetSubmissionAuditEventsRequest{ pub struct GetSubmissionAuditEventsRequest{
pub Page:u32, pub Page:u32,
@@ -420,7 +419,6 @@ pub struct GetSubmissionAuditEventsRequest{
pub SubmissionID:i64, pub SubmissionID:i64,
} }
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde_repr::Deserialize_repr)] #[derive(Clone,Debug,serde_repr::Deserialize_repr)]
#[repr(u32)] #[repr(u32)]
pub enum AuditEventType{ pub enum AuditEventType{
@@ -475,7 +473,6 @@ pub struct AuditEventCheckList{
pub check_list:Vec<AuditEventCheck>, pub check_list:Vec<AuditEventCheck>,
} }
#[allow(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub enum AuditEventData{ pub enum AuditEventData{
Action(AuditEventAction), Action(AuditEventAction),
@@ -491,7 +488,7 @@ pub enum AuditEventData{
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,serde::Serialize,serde::Deserialize)] #[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,serde::Serialize,serde::Deserialize)]
pub struct AuditEventID(pub(crate)i64); pub struct AuditEventID(pub(crate)i64);
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct AuditEventReponse{ pub struct AuditEventReponse{
pub ID:AuditEventID, pub ID:AuditEventID,
@@ -518,7 +515,7 @@ impl AuditEventReponse{
} }
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)] #[derive(Clone,Debug,serde::Serialize)]
pub struct Check{ pub struct Check{
pub Name:&'static str, pub Name:&'static str,
@@ -526,7 +523,7 @@ pub struct Check{
pub Passed:bool, pub Passed:bool,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct ActionSubmissionSubmittedRequest{ pub struct ActionSubmissionSubmittedRequest{
pub SubmissionID:SubmissionID, pub SubmissionID:SubmissionID,
@@ -536,33 +533,33 @@ pub struct ActionSubmissionSubmittedRequest{
pub GameID:GameID, pub GameID:GameID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct ActionSubmissionRequestChangesRequest{ pub struct ActionSubmissionRequestChangesRequest{
pub SubmissionID:SubmissionID, pub SubmissionID:SubmissionID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct ActionSubmissionUploadedRequest{ pub struct ActionSubmissionUploadedRequest{
pub SubmissionID:SubmissionID, pub SubmissionID:SubmissionID,
pub UploadedAssetID:u64, pub UploadedAssetID:u64,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct ActionSubmissionAcceptedRequest{ pub struct ActionSubmissionAcceptedRequest{
pub SubmissionID:SubmissionID, pub SubmissionID:SubmissionID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct CreateSubmissionAuditErrorRequest{ pub struct CreateSubmissionAuditErrorRequest{
pub SubmissionID:SubmissionID, pub SubmissionID:SubmissionID,
pub ErrorMessage:String, pub ErrorMessage:String,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct CreateSubmissionAuditCheckListRequest<'a>{ pub struct CreateSubmissionAuditCheckListRequest<'a>{
pub SubmissionID:SubmissionID, pub SubmissionID:SubmissionID,
@@ -580,7 +577,7 @@ impl SubmissionID{
} }
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct UpdateMapfixModelRequest{ pub struct UpdateMapfixModelRequest{
pub MapfixID:MapfixID, pub MapfixID:MapfixID,
@@ -588,7 +585,7 @@ pub struct UpdateMapfixModelRequest{
pub ModelVersion:u64, pub ModelVersion:u64,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct ActionMapfixSubmittedRequest{ pub struct ActionMapfixSubmittedRequest{
pub MapfixID:MapfixID, pub MapfixID:MapfixID,
@@ -598,32 +595,32 @@ pub struct ActionMapfixSubmittedRequest{
pub GameID:GameID, pub GameID:GameID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct ActionMapfixRequestChangesRequest{ pub struct ActionMapfixRequestChangesRequest{
pub MapfixID:MapfixID, pub MapfixID:MapfixID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct ActionMapfixUploadedRequest{ pub struct ActionMapfixUploadedRequest{
pub MapfixID:MapfixID, pub MapfixID:MapfixID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct ActionMapfixAcceptedRequest{ pub struct ActionMapfixAcceptedRequest{
pub MapfixID:MapfixID, pub MapfixID:MapfixID,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct CreateMapfixAuditErrorRequest{ pub struct CreateMapfixAuditErrorRequest{
pub MapfixID:MapfixID, pub MapfixID:MapfixID,
pub ErrorMessage:String, pub ErrorMessage:String,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct CreateMapfixAuditCheckListRequest<'a>{ pub struct CreateMapfixAuditCheckListRequest<'a>{
pub MapfixID:MapfixID, pub MapfixID:MapfixID,
@@ -641,7 +638,7 @@ impl MapfixID{
} }
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct ActionOperationFailedRequest{ pub struct ActionOperationFailedRequest{
pub OperationID:OperationID, pub OperationID:OperationID,
@@ -668,7 +665,7 @@ impl Resource{
} }
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)] #[derive(Clone,Debug,serde::Serialize)]
pub struct ReleaseInfo{ pub struct ReleaseInfo{
pub SubmissionID:SubmissionID, pub SubmissionID:SubmissionID,
@@ -678,7 +675,7 @@ pub struct ReleaseInfo{
pub struct ReleaseRequest<'a>{ pub struct ReleaseRequest<'a>{
pub schedule:&'a [ReleaseInfo], pub schedule:&'a [ReleaseInfo],
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)] #[derive(Clone,Debug,serde::Deserialize)]
pub struct OperationIDResponse{ pub struct OperationIDResponse{
pub OperationID:OperationID, pub OperationID:OperationID,

View File

@@ -6,7 +6,7 @@ use heck::{ToSnakeCase,ToTitleCase};
use rbx_dom_weak::Instance; use rbx_dom_weak::Instance;
use rust_grpc::validator::Check; use rust_grpc::validator::Check;
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
ModelInfoDownload(rbx_asset::cloud::GetError), ModelInfoDownload(rbx_asset::cloud::GetError),
@@ -33,7 +33,7 @@ macro_rules! lazy_regex{
}}; }};
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
pub struct CheckRequest{ pub struct CheckRequest{
ModelID:u64, ModelID:u64,
SkipChecks:bool, SkipChecks:bool,
@@ -79,7 +79,7 @@ struct ModeElement{
zone:Zone, zone:Zone,
mode_id:ModeID, mode_id:ModeID,
} }
#[allow(dead_code)] #[expect(dead_code)]
pub enum IDParseError{ pub enum IDParseError{
NoCaptures, NoCaptures,
ParseInt(core::num::ParseIntError), ParseInt(core::num::ParseIntError),
@@ -442,7 +442,7 @@ pub struct MapInfoOwned{
pub creator:String, pub creator:String,
pub game_id:GameID, pub game_id:GameID,
} }
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum IntoMapInfoOwnedError{ pub enum IntoMapInfoOwnedError{
DisplayName(StringValueError), DisplayName(StringValueError),

View File

@@ -1,7 +1,7 @@
use crate::check::CheckListAndVersion; use crate::check::CheckListAndVersion;
use crate::nats_types::CheckMapfixRequest; use crate::nats_types::CheckMapfixRequest;
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
Check(crate::check::Error), Check(crate::check::Error),

View File

@@ -1,7 +1,7 @@
use crate::check::CheckListAndVersion; use crate::check::CheckListAndVersion;
use crate::nats_types::CheckSubmissionRequest; use crate::nats_types::CheckSubmissionRequest;
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
Check(crate::check::Error), Check(crate::check::Error),

View File

@@ -1,7 +1,7 @@
use crate::download::download_asset_version; use crate::download::download_asset_version;
use crate::rbx_util::{get_root_instance,get_mapinfo,read_dom,MapInfo,ReadDomError,GetRootInstanceError,GameID}; use crate::rbx_util::{get_root_instance,get_mapinfo,read_dom,MapInfo,ReadDomError,GetRootInstanceError,GameID};
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
CreatorTypeMustBeUser, CreatorTypeMustBeUser,
@@ -17,11 +17,11 @@ impl std::fmt::Display for Error{
} }
impl std::error::Error for Error{} impl std::error::Error for Error{}
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
pub struct CreateRequest{ pub struct CreateRequest{
pub ModelID:u64, pub ModelID:u64,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
pub struct CreateResult{ pub struct CreateResult{
pub AssetOwner:u64, pub AssetOwner:u64,
pub DisplayName:Option<String>, pub DisplayName:Option<String>,

View File

@@ -1,7 +1,7 @@
use crate::nats_types::CreateMapfixRequest; use crate::nats_types::CreateMapfixRequest;
use crate::create::CreateRequest; use crate::create::CreateRequest;
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
Create(crate::create::Error), Create(crate::create::Error),

View File

@@ -2,7 +2,7 @@ use crate::nats_types::CreateSubmissionRequest;
use crate::create::CreateRequest; use crate::create::CreateRequest;
use crate::rbx_util::GameID; use crate::rbx_util::GameID;
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
Create(crate::create::Error), Create(crate::create::Error),

View File

@@ -1,4 +1,4 @@
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
ModelLocationDownload(rbx_asset::cloud::GetError), ModelLocationDownload(rbx_asset::cloud::GetError),

View File

@@ -22,7 +22,6 @@ mod validator;
mod validate_mapfix; mod validate_mapfix;
mod validate_submission; mod validate_submission;
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum StartupError{ pub enum StartupError{
API(tonic::transport::Error), API(tonic::transport::Error),

View File

@@ -1,4 +1,4 @@
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum HandleMessageError{ pub enum HandleMessageError{
Messages(async_nats::jetstream::consumer::pull::MessagesError), Messages(async_nats::jetstream::consumer::pull::MessagesError),

View File

@@ -4,7 +4,7 @@
// Requests are sent from maps-service to validator // Requests are sent from maps-service to validator
// Validation invokes the REST api to update the submissions // Validation invokes the REST api to update the submissions
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct CreateSubmissionRequest{ pub struct CreateSubmissionRequest{
// operation_id is passed back in the response message // operation_id is passed back in the response message
@@ -18,7 +18,7 @@ pub struct CreateSubmissionRequest{
pub Roles:u32, pub Roles:u32,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct CreateMapfixRequest{ pub struct CreateMapfixRequest{
pub OperationID:u32, pub OperationID:u32,
@@ -27,7 +27,7 @@ pub struct CreateMapfixRequest{
pub Description:String, pub Description:String,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct CheckSubmissionRequest{ pub struct CheckSubmissionRequest{
pub SubmissionID:u64, pub SubmissionID:u64,
@@ -35,7 +35,7 @@ pub struct CheckSubmissionRequest{
pub SkipChecks:bool, pub SkipChecks:bool,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct CheckMapfixRequest{ pub struct CheckMapfixRequest{
pub MapfixID:u64, pub MapfixID:u64,
@@ -43,7 +43,7 @@ pub struct CheckMapfixRequest{
pub SkipChecks:bool, pub SkipChecks:bool,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct ValidateSubmissionRequest{ pub struct ValidateSubmissionRequest{
// submission_id is passed back in the response message // submission_id is passed back in the response message
@@ -53,7 +53,7 @@ pub struct ValidateSubmissionRequest{
pub ValidatedModelID:Option<u64>, pub ValidatedModelID:Option<u64>,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct ValidateMapfixRequest{ pub struct ValidateMapfixRequest{
// submission_id is passed back in the response message // submission_id is passed back in the response message
@@ -64,7 +64,7 @@ pub struct ValidateMapfixRequest{
} }
// Create a new map // Create a new map
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct UploadSubmissionRequest{ pub struct UploadSubmissionRequest{
pub SubmissionID:u64, pub SubmissionID:u64,
@@ -73,7 +73,7 @@ pub struct UploadSubmissionRequest{
pub ModelName:String, pub ModelName:String,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct UploadMapfixRequest{ pub struct UploadMapfixRequest{
pub MapfixID:u64, pub MapfixID:u64,
@@ -83,7 +83,7 @@ pub struct UploadMapfixRequest{
} }
// Release a new map // Release a new map
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct ReleaseSubmissionRequest{ pub struct ReleaseSubmissionRequest{
pub SubmissionID:u64, pub SubmissionID:u64,
@@ -97,14 +97,14 @@ pub struct ReleaseSubmissionRequest{
pub Submitter:u64, pub Submitter:u64,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct ReleaseSubmissionsBatchRequest{ pub struct ReleaseSubmissionsBatchRequest{
pub Submissions:Vec<ReleaseSubmissionRequest>, pub Submissions:Vec<ReleaseSubmissionRequest>,
pub OperationID:u32, pub OperationID:u32,
} }
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct ReleaseMapfixRequest{ pub struct ReleaseMapfixRequest{
pub MapfixID:u64, pub MapfixID:u64,

View File

@@ -1,4 +1,4 @@
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum ReadDomError{ pub enum ReadDomError{
Binary(rbx_binary::DecodeError), Binary(rbx_binary::DecodeError),

View File

@@ -183,7 +183,7 @@ async fn release_inner(
Ok(()) Ok(())
} }
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
UpdateOperation(tonic::Status), UpdateOperation(tonic::Status),

View File

@@ -1,7 +1,7 @@
use crate::download::download_asset_version; use crate::download::download_asset_version;
use crate::nats_types::UploadMapfixRequest; use crate::nats_types::UploadMapfixRequest;
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum InnerError{ pub enum InnerError{
Download(crate::download::Error), Download(crate::download::Error),
@@ -43,7 +43,7 @@ async fn upload_inner(
Ok(()) Ok(())
} }
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
ApiActionMapfixUploaded(tonic::Status), ApiActionMapfixUploaded(tonic::Status),

View File

@@ -1,7 +1,7 @@
use crate::download::download_asset_version; use crate::download::download_asset_version;
use crate::nats_types::UploadSubmissionRequest; use crate::nats_types::UploadSubmissionRequest;
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum InnerError{ pub enum InnerError{
Download(crate::download::Error), Download(crate::download::Error),
@@ -44,7 +44,7 @@ async fn upload_inner(
Ok(upload_response.AssetId) Ok(upload_response.AssetId)
} }
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
ApiActionSubmissionUploaded(tonic::Status), ApiActionSubmissionUploaded(tonic::Status),

View File

@@ -1,6 +1,6 @@
use crate::nats_types::ValidateMapfixRequest; use crate::nats_types::ValidateMapfixRequest;
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
ApiActionMapfixValidate(tonic::Status), ApiActionMapfixValidate(tonic::Status),

View File

@@ -1,6 +1,6 @@
use crate::nats_types::ValidateSubmissionRequest; use crate::nats_types::ValidateSubmissionRequest;
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
ApiActionSubmissionValidate(tonic::Status), ApiActionSubmissionValidate(tonic::Status),

View File

@@ -17,7 +17,7 @@ fn hash_source(source:&str)->u64{
std::hash::Hasher::finish(&hasher) std::hash::Hasher::finish(&hasher)
} }
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
ModelInfoDownload(rbx_asset::cloud::GetError), ModelInfoDownload(rbx_asset::cloud::GetError),
@@ -52,7 +52,7 @@ impl std::fmt::Display for Error{
} }
impl std::error::Error for Error{} impl std::error::Error for Error{}
#[allow(nonstandard_style)] #[expect(nonstandard_style)]
pub struct ValidateRequest{ pub struct ValidateRequest{
pub ModelID:u64, pub ModelID:u64,
pub ModelVersion:u64, pub ModelVersion:u64,

34
web/.gitignore vendored
View File

@@ -1,24 +1,12 @@
bun.lockb
# dependencies # dependencies
/node_modules /node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing # testing
/coverage /coverage
# next.js
/.next/
/out/
# production # production
/build /build
/dist
# misc # misc
.DS_Store .DS_Store
@@ -29,12 +17,22 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# env files (can opt-in for committing if needed) # env files
.env* .env*
.env.local
# vercel .env.development.local
.vercel .env.test.local
.env.production.local
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts
# editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,13 +1,29 @@
FROM registry.itzana.me/docker-proxy/oven/bun:1.3.3 # Build stage
FROM registry.itzana.me/docker-proxy/oven/bun:1.3.3 AS builder
WORKDIR /app WORKDIR /app
COPY package.json bun.lockb* ./
RUN bun install --frozen-lockfile
COPY . . COPY . .
RUN bun run build
# Release
FROM registry.itzana.me/docker-proxy/nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
# Add nginx configuration for SPA routing
RUN echo 'server { \
listen 3000; \
location / { \
root /usr/share/nginx/html; \
index index.html; \
try_files $uri $uri/ /index.html; \
} \
}' > /etc/nginx/conf.d/default.conf
EXPOSE 3000 EXPOSE 3000
ENV NEXT_TELEMETRY_DISABLED=1 CMD ["nginx", "-g", "daemon off;"]
RUN bun install
RUN bun run build
ENTRYPOINT ["bun", "run", "start"]

File diff suppressed because it is too large Load Diff

13
web/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Maps Service</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,16 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
distDir: "build",
output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**.rbxcdn.com",
},
],
},
};
export default nextConfig;

4142
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,21 +2,24 @@
"name": "map-service-web", "name": "map-service-web",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "next dev -p 3000 --turbopack", "dev": "vite",
"build": "next build", "build": "tsc && vite build",
"start": "next start -p 3000", "preview": "vite preview",
"lint": "next lint" "lint": "eslint src --ext ts,tsx"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^7.3.6", "@mui/icons-material": "^7.3.6",
"@mui/material": "^7.3.6", "@mui/material": "^7.3.6",
"@tanstack/react-query": "^5.90.12",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"next": "^16.0.7",
"react": "^19.2.1", "react": "^19.2.1",
"react-dom": "^19.2.1", "react-dom": "^19.2.1",
"react-router-dom": "^7.1.3",
"sass": "^1.94.2" "sass": "^1.94.2"
}, },
"devDependencies": { "devDependencies": {
@@ -24,8 +27,9 @@
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-config-next": "16.0.7", "typescript": "^5.9.3",
"typescript": "^5.9.3" "vite": "^6.0.7"
} }
} }

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

46
web/src/App.tsx Normal file
View File

@@ -0,0 +1,46 @@
import { Routes, Route } from 'react-router-dom'
import { ThemeProvider } from '@mui/material'
import { theme } from '@/app/lib/theme'
// Pages
import Home from '@/app/page'
import MapsPage from '@/app/maps/page'
import MapDetailPage from '@/app/maps/[mapId]/page'
import MapFixCreatePage from '@/app/maps/[mapId]/fix/page'
import MapfixesPage from '@/app/mapfixes/page'
import MapfixDetailPage from '@/app/mapfixes/[mapfixId]/page'
import SubmissionsPage from '@/app/submissions/page'
import SubmissionDetailPage from '@/app/submissions/[submissionId]/page'
import SubmitPage from '@/app/submit/page'
import AdminSubmitPage from '@/app/admin-submit/page'
import OperationPage from '@/app/operations/[operationId]/page'
import ReviewerDashboardPage from '@/app/reviewer-dashboard/page'
import UserDashboardPage from '@/app/user-dashboard/page'
import ScriptReviewPage from '@/app/script-review/page'
import NotFound from '@/app/not-found/page'
function App() {
return (
<ThemeProvider theme={theme}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/maps" element={<MapsPage />} />
<Route path="/maps/:mapId" element={<MapDetailPage />} />
<Route path="/maps/:mapId/fix" element={<MapFixCreatePage />} />
<Route path="/mapfixes" element={<MapfixesPage />} />
<Route path="/mapfixes/:mapfixId" element={<MapfixDetailPage />} />
<Route path="/submissions" element={<SubmissionsPage />} />
<Route path="/submissions/:submissionId" element={<SubmissionDetailPage />} />
<Route path="/submit" element={<SubmitPage />} />
<Route path="/admin-submit" element={<AdminSubmitPage />} />
<Route path="/operations/:operationId" element={<OperationPage />} />
<Route path="/review" element={<ReviewerDashboardPage />} />
<Route path="/dashboard" element={<UserDashboardPage />} />
<Route path="/script-review" element={<ScriptReviewPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</ThemeProvider>
)
}
export default App

View File

@@ -1,6 +1,6 @@
import {Box, IconButton, Typography} from "@mui/material"; import {Box, Button, IconButton, Typography} from "@mui/material";
import {useEffect, useRef, useState} from "react"; import {useEffect, useRef, useState} from "react";
import Link from "next/link"; import { Link } from "react-router-dom";
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import {SubmissionInfo} from "@/app/ts/Submission"; import {SubmissionInfo} from "@/app/ts/Submission";
@@ -65,14 +65,22 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
return ( return (
<Box mb={6}> <Box mb={6}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}> <Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
<Typography variant="h4" component="h2" fontWeight="bold"> <Typography variant="h4" component="h2" fontWeight="bold">
{title} {title}
</Typography> </Typography>
<Link href={viewAllLink} style={{textDecoration: 'none'}}> <Link to={viewAllLink} style={{textDecoration: 'none'}}>
<Typography component="span" color="primary"> <Button
View All endIcon={<ArrowForwardIosIcon sx={{ fontSize: '0.875rem' }} />}
</Typography> sx={{
color: 'primary.main',
'&:hover': {
backgroundColor: 'rgba(99, 102, 241, 0.1)',
},
}}
>
View All
</Button>
</Link> </Link>
</Box> </Box>
@@ -85,9 +93,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
zIndex: 2, zIndex: 2,
backgroundColor: 'background.paper', backgroundColor: 'background.paper',
boxShadow: 2, border: '1px solid rgba(99, 102, 241, 0.2)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
'&:hover': { '&:hover': {
backgroundColor: 'action.hover', backgroundColor: 'background.paper',
borderColor: 'rgba(99, 102, 241, 0.4)',
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
}, },
visibility: scrollPosition <= 5 ? 'hidden' : 'visible', visibility: scrollPosition <= 5 ? 'hidden' : 'visible',
}} }}
@@ -106,7 +117,7 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
'&::-webkit-scrollbar': { '&::-webkit-scrollbar': {
display: 'none', display: 'none',
}, },
gap: '16px', // Fixed 16px gap - using string with px unit to ensure it's absolute gap: '20px',
padding: '8px 4px', padding: '8px 4px',
}} }}
> >
@@ -116,7 +127,7 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
sx={{ sx={{
flex: '0 0 auto', flex: '0 0 auto',
width: { width: {
xs: '260px', // Fixed width at different breakpoints xs: '260px',
sm: '280px', sm: '280px',
md: '300px' md: '300px'
} }
@@ -135,9 +146,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
zIndex: 2, zIndex: 2,
backgroundColor: 'background.paper', backgroundColor: 'background.paper',
boxShadow: 2, border: '1px solid rgba(99, 102, 241, 0.2)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
'&:hover': { '&:hover': {
backgroundColor: 'action.hover', backgroundColor: 'background.paper',
borderColor: 'rgba(99, 102, 241, 0.4)',
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
}, },
visibility: scrollPosition >= maxScroll - 5 ? 'hidden' : 'visible', visibility: scrollPosition >= maxScroll - 5 ? 'hidden' : 'visible',
}} }}

View File

@@ -1,13 +1,14 @@
import React from 'react';
import { import {
Box, Box,
Avatar, Avatar,
Typography, Typography,
Tooltip Tooltip,
Skeleton
} from "@mui/material"; } from "@mui/material";
import PersonIcon from '@mui/icons-material/Person'; import PersonIcon from '@mui/icons-material/Person';
import { formatDistanceToNow, format } from "date-fns"; import { formatDistanceToNow, format } from "date-fns";
import { AuditEvent, decodeAuditEvent as auditEventMessage } from "@/app/ts/AuditEvent"; import { AuditEvent, decodeAuditEvent as auditEventMessage } from "@/app/ts/AuditEvent";
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
interface AuditEventItemProps { interface AuditEventItemProps {
event: AuditEvent; event: AuditEvent;
@@ -15,17 +16,44 @@ interface AuditEventItemProps {
} }
export default function AuditEventItem({ event, validatorUser }: AuditEventItemProps) { export default function AuditEventItem({ event, validatorUser }: AuditEventItemProps) {
const isValidator = event.User === validatorUser;
const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150');
return ( return (
<Box sx={{ display: 'flex', gap: 2 }}> <Box sx={{
<Avatar display: 'flex',
src={event.User === validatorUser ? undefined : `/thumbnails/user/${event.User}`} gap: 2,
> p: 2,
<PersonIcon /> borderRadius: 1
</Avatar> }}>
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
<Skeleton
variant="circular"
sx={{
position: 'absolute',
width: 40,
height: 40,
opacity: isLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
animation="wave"
/>
<Avatar
src={isValidator ? undefined : (thumbnailUrl || undefined)}
sx={{
width: 40,
height: 40,
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}
>
<PersonIcon />
</Avatar>
</Box>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="subtitle2"> <Typography variant="subtitle2">
{event.User === validatorUser ? "Validator" : event.Username || "Unknown"} {isValidator ? "Validator" : event.Username || "Unknown"}
</Typography> </Typography>
<DateDisplay date={event.Date} /> <DateDisplay date={event.Date} />
</Box> </Box>

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { import {
Box, Box,
Stack, Stack,
@@ -22,18 +21,21 @@ export default function AuditEventsTabPanel({
); );
return ( return (
<Box role="tabpanel" hidden={activeTab !== 1}> <Box
{activeTab === 1 && ( role="tabpanel"
<Stack spacing={2}> sx={{
{filteredEvents.map((event, index) => ( display: activeTab === 1 ? 'block' : 'none'
<AuditEventItem }}
key={index} >
event={event} <Stack spacing={2}>
validatorUser={validatorUser} {filteredEvents.map((event, index) => (
/> <AuditEventItem
))} key={index}
</Stack> event={event}
)} validatorUser={validatorUser}
/>
))}
</Stack>
</Box> </Box>
); );
} }

View File

@@ -1,13 +1,14 @@
import React from 'react';
import { import {
Box, Box,
Avatar, Avatar,
Typography, Typography,
Tooltip Tooltip,
Skeleton
} from "@mui/material"; } from "@mui/material";
import PersonIcon from '@mui/icons-material/Person'; import PersonIcon from '@mui/icons-material/Person';
import { formatDistanceToNow, format } from "date-fns"; import { formatDistanceToNow, format } from "date-fns";
import { AuditEvent, decodeAuditEvent } from "@/app/ts/AuditEvent"; import { AuditEvent, decodeAuditEvent } from "@/app/ts/AuditEvent";
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
interface CommentItemProps { interface CommentItemProps {
event: AuditEvent; event: AuditEvent;
@@ -15,21 +16,43 @@ interface CommentItemProps {
} }
export default function CommentItem({ event, validatorUser }: CommentItemProps) { export default function CommentItem({ event, validatorUser }: CommentItemProps) {
const isValidator = event.User === validatorUser;
const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150');
return ( return (
<Box sx={{ display: 'flex', gap: 2 }}> <Box sx={{ display: 'flex', gap: 2 }}>
<Avatar <Box sx={{ position: 'relative', width: 40, height: 40 }}>
src={event.User === validatorUser ? undefined : `/thumbnails/user/${event.User}`} <Skeleton
> variant="circular"
<PersonIcon /> sx={{
</Avatar> position: 'absolute',
width: 40,
height: 40,
opacity: isLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
animation="wave"
/>
<Avatar
src={isValidator ? undefined : (thumbnailUrl || undefined)}
sx={{
width: 40,
height: 40,
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}
>
<PersonIcon />
</Avatar>
</Box>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="subtitle2"> <Typography variant="subtitle2">
{event.User === validatorUser ? "Validator" : event.Username || "Unknown"} {isValidator ? "Validator" : event.Username || "Unknown"}
</Typography> </Typography>
<DateDisplay date={event.Date} /> <DateDisplay date={event.Date} />
</Box> </Box>
<Typography variant="body2">{decodeAuditEvent(event)}</Typography> <Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>{decodeAuditEvent(event)}</Typography>
</Box> </Box>
</Box> </Box>
); );

View File

@@ -4,10 +4,22 @@ import {
Box, Box,
Tabs, Tabs,
Tab, Tab,
keyframes
} from "@mui/material"; } from "@mui/material";
import CommentsTabPanel from './CommentsTabPanel'; import CommentsTabPanel from './CommentsTabPanel';
import AuditEventsTabPanel from './AuditEventsTabPanel'; import AuditEventsTabPanel from './AuditEventsTabPanel';
import { AuditEvent } from "@/app/ts/AuditEvent"; import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
const pulse = keyframes`
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.1);
}
`;
interface CommentsAndAuditSectionProps { interface CommentsAndAuditSectionProps {
auditEvents: AuditEvent[]; auditEvents: AuditEvent[];
@@ -16,6 +28,7 @@ interface CommentsAndAuditSectionProps {
handleCommentSubmit: () => void; handleCommentSubmit: () => void;
validatorUser: number; validatorUser: number;
userId: number | null; userId: number | null;
currentStatus?: number;
} }
export default function CommentsAndAuditSection({ export default function CommentsAndAuditSection({
@@ -25,13 +38,24 @@ export default function CommentsAndAuditSection({
handleCommentSubmit, handleCommentSubmit,
validatorUser, validatorUser,
userId, userId,
currentStatus,
}: CommentsAndAuditSectionProps) { }: CommentsAndAuditSectionProps) {
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setActiveTab(newValue); setActiveTab(newValue);
}; };
// Check if there's validator feedback for changes requested status
// Show badge if status is ChangesRequested and there are validator events
const hasValidatorFeedback = currentStatus === 1 && auditEvents.some(event =>
event.User === validatorUser &&
(
event.EventType === AuditEventType.Error ||
event.EventType === AuditEventType.CheckList
)
);
return ( return (
<Paper sx={{ p: 3, mt: 3 }}> <Paper sx={{ p: 3, mt: 3 }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}> <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
@@ -41,7 +65,24 @@ export default function CommentsAndAuditSection({
aria-label="comments and audit tabs" aria-label="comments and audit tabs"
> >
<Tab label="Comments" /> <Tab label="Comments" />
<Tab label="Audit Events" /> <Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
Audit Events
{hasValidatorFeedback && (
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: '#ff9800',
animation: `${pulse} 2s ease-in-out infinite`
}}
/>
)}
</Box>
}
/>
</Tabs> </Tabs>
</Box> </Box>

View File

@@ -1,14 +1,15 @@
import React from 'react';
import { import {
Box, Box,
Stack, Stack,
Avatar, Avatar,
TextField, TextField,
IconButton IconButton,
Skeleton
} from "@mui/material"; } from "@mui/material";
import SendIcon from '@mui/icons-material/Send'; import SendIcon from '@mui/icons-material/Send';
import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent"; import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
import CommentItem from './CommentItem'; import CommentItem from './CommentItem';
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
interface CommentsTabPanelProps { interface CommentsTabPanelProps {
activeTab: number; activeTab: number;
@@ -34,34 +35,35 @@ export default function CommentsTabPanel({
); );
return ( return (
<Box role="tabpanel" hidden={activeTab !== 0}> <Box
{activeTab === 0 && ( role="tabpanel"
<> sx={{
<Stack spacing={2} sx={{ mb: 3 }}> display: activeTab === 0 ? 'block' : 'none'
{commentEvents.length > 0 ? ( }}
commentEvents.map((event, index) => ( >
<CommentItem <Stack spacing={2} sx={{ mb: 3 }}>
key={index} {commentEvents.length > 0 ? (
event={event} commentEvents.map((event, index) => (
validatorUser={validatorUser} <CommentItem
/> key={index}
)) event={event}
) : ( validatorUser={validatorUser}
<Box sx={{ textAlign: 'center', py: 2, color: 'text.secondary' }}>
No Comments
</Box>
)}
</Stack>
{userId !== null && (
<CommentInput
newComment={newComment}
setNewComment={setNewComment}
handleCommentSubmit={handleCommentSubmit}
userId={userId}
/> />
)} ))
</> ) : (
<Box sx={{ textAlign: 'center', py: 2, color: 'text.secondary' }}>
No Comments
</Box>
)}
</Stack>
{userId !== null && (
<CommentInput
newComment={newComment}
setNewComment={setNewComment}
handleCommentSubmit={handleCommentSubmit}
userId={userId}
/>
)} )}
</Box> </Box>
); );
@@ -75,11 +77,32 @@ interface CommentInputProps {
} }
function CommentInput({ newComment, setNewComment, handleCommentSubmit, userId }: CommentInputProps) { function CommentInput({ newComment, setNewComment, handleCommentSubmit, userId }: CommentInputProps) {
const { thumbnailUrl, isLoading } = useUserThumbnail(userId || undefined, '150x150');
return ( return (
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}> <Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
<Avatar <Box sx={{ position: 'relative', width: 40, height: 40 }}>
src={`/thumbnails/user/${userId}`} <Skeleton
/> variant="circular"
sx={{
position: 'absolute',
width: 40,
height: 40,
opacity: isLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
animation="wave"
/>
<Avatar
src={thumbnailUrl || undefined}
sx={{
width: 40,
height: 40,
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}
/>
</Box>
<TextField <TextField
fullWidth fullWidth
multiline multiline

View File

@@ -1,9 +1,6 @@
"use client" import { Link } from "react-router-dom"
import { useState, useRef } from "react";
import Link from "next/link" import { useUser } from "@/app/hooks/useUser";
import Image from "next/image";
import { UserInfo } from "@/app/ts/User";
import { useState, useEffect } from "react";
import AppBar from "@mui/material/AppBar"; import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
@@ -37,7 +34,7 @@ const navItems: HeaderButton[] = [
function HeaderButton(header: HeaderButton) { function HeaderButton(header: HeaderButton) {
return ( return (
<Button color="inherit" component={Link} href={header.href}> <Button color="inherit" component={Link} to={header.href}>
{header.name} {header.name}
</Button> </Button>
); );
@@ -47,14 +44,26 @@ export default function Header() {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const hasAnimated = useRef(false);
const handleLoginClick = () => { const getAuthUrl = () => {
window.location.href = const hostname = window.location.hostname;
"/auth/oauth2/login?redirect=" + window.location.href;
// Production only
if (hostname === 'maps.strafes.net') {
return 'https://auth.strafes.net';
}
// Default to staging (works for staging.strafes.net and localhost)
return 'https://auth.staging.strafes.net';
}; };
const [valid, setValid] = useState<boolean>(false); const handleLoginClick = () => {
const [user, setUser] = useState<UserInfo | null>(null); const authUrl = getAuthUrl();
window.location.href = `${authUrl}/oauth2/login?redirect=${window.location.href}`;
};
const { user, isLoggedIn } = useUser();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [quickLinksAnchor, setQuickLinksAnchor] = useState<null | HTMLElement>(null); const [quickLinksAnchor, setQuickLinksAnchor] = useState<null | HTMLElement>(null);
@@ -77,60 +86,34 @@ export default function Header() {
setQuickLinksAnchor(null); setQuickLinksAnchor(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();
}, []);
// Mobile navigation drawer content // Mobile navigation drawer content
const drawer = ( const drawer = (
<Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}> <Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}>
<List> <List>
{navItems.map((item) => ( {navItems.map((item) => (
<ListItem key={item.name} disablePadding> <ListItem key={item.name} disablePadding>
<ListItemButton component={Link} href={item.href} sx={{ textAlign: 'center' }}> <ListItemButton component={Link} to={item.href} sx={{ textAlign: 'center' }}>
<ListItemText primary={item.name} /> <ListItemText primary={item.name} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
))} ))}
{valid && user && ( {isLoggedIn && user && (
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton component={Link} href="/submit" sx={{ textAlign: 'center' }}> <ListItemButton component={Link} to="/submit" sx={{ textAlign: 'center' }}>
<ListItemText primary="Submit Map" sx={{ color: 'success.main' }} /> <ListItemText primary="Submit Map" sx={{ color: 'success.main' }} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
)} )}
{!valid && ( {!isLoggedIn && (
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton onClick={handleLoginClick} sx={{ textAlign: 'center' }}> <ListItemButton onClick={handleLoginClick} sx={{ textAlign: 'center' }}>
<ListItemText primary="Login" /> <ListItemText primary="Login" />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
)} )}
{valid && user && ( {isLoggedIn && user && (
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton component={Link} href="/auth" sx={{ textAlign: 'center' }}> <ListItemButton component="a" href={getAuthUrl()} sx={{ textAlign: 'center' }}>
<ListItemText primary="Manage Account" /> <ListItemText primary="Manage Account" />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
@@ -150,7 +133,7 @@ export default function Header() {
return ( return (
<AppBar position="static"> <AppBar position="static">
<Toolbar> <Toolbar sx={{ py: 1 }}>
{isMobile && ( {isMobile && (
<IconButton <IconButton
color="inherit" color="inherit"
@@ -165,20 +148,144 @@ export default function Header() {
{/* Desktop navigation */} {/* Desktop navigation */}
{!isMobile && ( {!isMobile && (
<Box display="flex" flexGrow={1} gap={2} alignItems="center"> <Box display="flex" flexGrow={1} gap={1} alignItems="center">
{/* Logo/Brand */}
<Box
component={Link}
to="/"
sx={{
mr: 4,
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
position: 'relative',
overflow: 'hidden',
'@keyframes speedLine': {
'0%': {
transform: 'translateX(-50px) scaleX(0.5)',
opacity: 0,
},
'40%': {
opacity: 0.8,
transform: 'translateX(0px) scaleX(1)',
},
'100%': {
opacity: 0,
transform: 'translateX(30px) scaleX(0.7)',
},
},
'@keyframes logoReveal': {
'0%': {
opacity: 0,
transform: 'translateX(-10px)',
filter: 'blur(2px)',
},
'100%': {
opacity: 1,
transform: 'translateX(0px)',
filter: 'blur(0px)',
},
},
'&::before, &::after': {
content: '""',
position: 'absolute',
left: 0,
width: '100%',
height: '2px',
background: 'linear-gradient(90deg, transparent 10%, rgba(59, 130, 246, 0.8) 50%, transparent 90%)',
pointerEvents: 'none',
animation: !hasAnimated.current ? 'speedLine 0.6s ease-out forwards' : 'none',
opacity: !hasAnimated.current ? 0 : undefined,
},
'&::before': {
top: '35%',
animationDelay: !hasAnimated.current ? '0s' : undefined,
},
'&::after': {
top: '65%',
animationDelay: !hasAnimated.current ? '0.08s' : undefined,
},
}}
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: 0,
width: '100%',
height: '1px',
background: 'linear-gradient(90deg, transparent 10%, rgba(139, 92, 246, 0.6) 50%, transparent 90%)',
animation: !hasAnimated.current ? 'speedLine 0.6s ease-out forwards' : 'none',
animationDelay: !hasAnimated.current ? '0.04s' : '0s',
opacity: !hasAnimated.current ? 0 : undefined,
pointerEvents: 'none',
}}
/>
<Typography
variant="h6"
sx={{
color: 'text.primary',
fontWeight: 700,
letterSpacing: '-0.01em',
fontSize: '1.125rem',
position: 'relative',
zIndex: 1,
opacity: !hasAnimated.current ? 0 : 1,
animation: !hasAnimated.current ? 'logoReveal 0.5s ease-out forwards' : 'none',
animationDelay: !hasAnimated.current ? '0.5s' : '0s',
}}
onAnimationEnd={() => {
hasAnimated.current = true;
}}
>
StrafesNET
</Typography>
</Box>
{navItems.map((item) => ( {navItems.map((item) => (
<HeaderButton key={item.name} name={item.name} href={item.href} /> <Button
key={item.name}
color="inherit"
component={Link}
to={item.href}
sx={{
px: 2,
py: 1,
borderRadius: 1.5,
fontSize: '0.9rem',
fontWeight: 500,
color: 'text.secondary',
transition: 'all 0.2s',
'&:hover': {
backgroundColor: 'rgba(59, 130, 246, 0.08)',
color: 'text.primary',
},
}}
>
{item.name}
</Button>
))} ))}
<Box sx={{ flexGrow: 1 }} /> {/* Push quick links to the right */} <Box sx={{ flexGrow: 1 }} />
{/* Quick Links Dropdown */} {/* Quick Links Dropdown */}
<Box> <Box>
<Button <Button
color="inherit" color="inherit"
endIcon={<ArrowDropDownIcon />} endIcon={<ArrowDropDownIcon />}
onClick={handleQuickLinksOpen} onClick={handleQuickLinksOpen}
sx={{ textTransform: 'none', fontSize: '0.95rem', px: 1 }} sx={{
px: 2,
mr: 1,
borderRadius: 1.5,
fontSize: '0.9rem',
fontWeight: 500,
color: 'text.secondary',
transition: 'all 0.2s',
'&:hover': {
backgroundColor: 'rgba(59, 130, 246, 0.08)',
color: 'text.primary',
},
}}
> >
QUICK LINKS Quick Links
</Button> </Button>
<Menu <Menu
anchorEl={quickLinksAnchor} anchorEl={quickLinksAnchor}
@@ -186,12 +293,20 @@ export default function Header() {
onClose={handleQuickLinksClose} onClose={handleQuickLinksClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }}
sx={{
'& .MuiMenu-paper': {
mt: 1.5,
},
}}
> >
{quickLinks.map(link => ( {quickLinks.map(link => (
<MenuItem <MenuItem
key={link.name} key={link.name}
onClick={handleQuickLinksClose} onClick={handleQuickLinksClose}
sx={{ minWidth: 180 }} sx={{
minWidth: 200,
fontSize: '0.9rem',
}}
component="a" component="a"
href={link.href} href={link.href}
target="_blank" target="_blank"
@@ -209,30 +324,53 @@ export default function Header() {
{isMobile && <Box sx={{ flexGrow: 1 }} />} {isMobile && <Box sx={{ flexGrow: 1 }} />}
{/* Right side of nav */} {/* Right side of nav */}
<Box display="flex" gap={2}> <Box display="flex" gap={2} alignItems="center">
{!isMobile && valid && user && ( {!isMobile && isLoggedIn && user && (
<Button variant="outlined" color="success" component={Link} href="/submit"> <Button
variant="contained"
color="primary"
component={Link}
to="/submit"
sx={{
px: 3,
}}
>
Submit Map Submit Map
</Button> </Button>
)} )}
{!isMobile && valid && user ? ( {!isMobile && isLoggedIn && user ? (
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Button <Button
onClick={handleMenuOpen} onClick={handleMenuOpen}
color="inherit" color="inherit"
size="small" size="small"
style={{ textTransform: "none" }} sx={{
textTransform: "none",
borderRadius: 1.5,
px: 1.5,
py: 0.75,
border: '1px solid rgba(255, 255, 255, 0.08)',
transition: 'all 0.2s',
'&:hover': {
backgroundColor: 'rgba(59, 130, 246, 0.08)',
borderColor: 'rgba(59, 130, 246, 0.3)',
},
}}
> >
<Image <img
className="avatar" className="avatar"
width={28} width={28}
height={28} height={28}
priority={true}
src={user.AvatarURL} src={user.AvatarURL}
alt={user.Username} alt={user.Username}
style={{ marginRight: 8 }} style={{
marginRight: 8,
borderRadius: '50%',
}}
/> />
<Typography variant="body1">{user.Username}</Typography> <Typography variant="body2" sx={{ fontSize: '0.875rem', fontWeight: 500 }}>
{user.Username}
</Typography>
</Button> </Button>
<Menu <Menu
anchorEl={anchorEl} anchorEl={anchorEl}
@@ -240,38 +378,58 @@ export default function Header() {
onClose={handleMenuClose} onClose={handleMenuClose}
anchorOrigin={{ anchorOrigin={{
vertical: "bottom", vertical: "bottom",
horizontal: "left", horizontal: "right",
}} }}
transformOrigin={{ transformOrigin={{
vertical: "top", vertical: "top",
horizontal: "left", horizontal: "right",
}}
sx={{
'& .MuiMenu-paper': {
mt: 1.5,
},
}} }}
> >
<MenuItem component={Link} href="/auth"> <MenuItem
Manage component="a"
href={getAuthUrl()}
sx={{
fontSize: '0.9rem',
}}
>
Manage Account
</MenuItem> </MenuItem>
</Menu> </Menu>
</Box> </Box>
) : !isMobile && ( ) : !isMobile && (
<Button color="inherit" onClick={handleLoginClick}> <Button
variant="outlined"
color="primary"
onClick={handleLoginClick}
sx={{
px: 3,
}}
>
Login Login
</Button> </Button>
)} )}
{/* In mobile view, display just the avatar if logged in */} {/* In mobile view, display just the avatar if logged in */}
{isMobile && valid && user && ( {isMobile && isLoggedIn && user && (
<IconButton <IconButton
onClick={handleMenuOpen} onClick={handleMenuOpen}
color="inherit" color="inherit"
size="small" size="small"
> >
<Image <img
className="avatar" className="avatar"
width={28} width={32}
height={28} height={32}
priority={true}
src={user.AvatarURL} src={user.AvatarURL}
alt={user.Username} alt={user.Username}
style={{
borderRadius: '50%',
}}
/> />
</IconButton> </IconButton>
)} )}
@@ -284,10 +442,13 @@ export default function Header() {
open={mobileOpen} open={mobileOpen}
onClose={handleDrawerToggle} onClose={handleDrawerToggle}
ModalProps={{ ModalProps={{
keepMounted: true, // Better open performance on mobile keepMounted: true,
}} }}
sx={{ sx={{
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: 240 }, '& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: 240,
},
}} }}
> >
{drawer} {drawer}

View File

@@ -1,7 +1,10 @@
import React from "react"; import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Divider, Typography, Skeleton} from "@mui/material";
import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Divider, Grid, Typography} from "@mui/material"; import {Explore, Person2, Assignment, Build} from "@mui/icons-material";
import {Explore, Person2} from "@mui/icons-material";
import {StatusChip} from "@/app/_components/statusChip"; import {StatusChip} from "@/app/_components/statusChip";
import {Link} from "react-router-dom";
import {useAssetThumbnail, useUserThumbnail} from "@/app/hooks/useThumbnails";
import {useUsername} from "@/app/hooks/useUsername";
import { getGameName } from "@/app/utils/games";
interface MapCardProps { interface MapCardProps {
displayName: string; displayName: string;
@@ -14,173 +17,176 @@ interface MapCardProps {
gameID: number; gameID: number;
created: number; created: number;
type: 'mapfix' | 'submission'; type: 'mapfix' | 'submission';
showTypeBadge?: boolean;
} }
const CARD_WIDTH = 270;
export function MapCard(props: MapCardProps) { export function MapCard(props: MapCardProps) {
const { thumbnailUrl: assetThumbnail, isLoading: assetLoading } = useAssetThumbnail(props.assetId);
const { thumbnailUrl: userThumbnail, isLoading: userLoading } = useUserThumbnail(props.authorId);
const { username, isLoading: usernameLoading } = useUsername(props.type === 'mapfix' ? props.authorId : undefined);
return ( return (
<Grid size={{ xs: 12, sm: 6, md: 3 }} key={props.assetId}> <Card sx={{ height: '100%' }}>
<Box sx={{ <CardActionArea
width: CARD_WIDTH, component={Link}
mx: 'auto', // Center the card in its grid cell to={`/${props.type === 'submission' ? 'submissions' : 'mapfixes'}/${props.id}`}>
}}> <Box sx={{ position: 'relative', overflow: 'hidden' }}>
<Card sx={{ <Skeleton
width: CARD_WIDTH, variant="rectangular"
height: 340, // Fixed height for all cards height={180}
display: 'flex', animation="wave"
flexDirection: 'column',
}}>
<CardActionArea
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch'
}}
href={`/${props.type === 'submission' ? 'submissions' : 'mapfixes'}/${props.id}`}>
<Box sx={{ position: 'relative' }}>
<CardMedia
component="img"
image={`/thumbnails/asset/${props.assetId}`}
alt={props.displayName}
sx={{ sx={{
height: 160, // Fixed height for all images position: 'absolute',
objectFit: 'cover', top: 0,
left: 0,
width: '100%',
height: '100%',
opacity: assetLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}} }}
/> />
<CardMedia
component="img"
image={assetThumbnail || '/placeholder-map.png'}
alt={props.displayName}
sx={{
height: 180,
objectFit: 'cover',
opacity: assetLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out, transform 0.3s',
'&:hover': {
transform: 'scale(1.05)',
},
}}
/>
{props.showTypeBadge && (
<Box
sx={{
position: 'absolute',
top: 12,
left: 12,
opacity: assetLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out 0.1s',
bgcolor: props.type === 'submission' ? 'primary.main' : 'secondary.main',
color: 'white',
borderRadius: '50%',
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: 2
}}
>
{props.type === 'submission' ? <Assignment sx={{ fontSize: '1.1rem' }} /> : <Build sx={{ fontSize: '1.1rem' }} />}
</Box>
)}
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: 12, top: 12,
right: 12, right: 12,
opacity: assetLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out 0.1s',
}} }}
> >
<StatusChip status={props.statusID}/> <StatusChip status={props.statusID}/>
</Box> </Box>
</Box> </Box>
<CardContent sx={{ <CardContent>
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
p: 2,
width: '100%',
}}>
<Box> <Box>
<Typography <Typography
variant="subtitle1" variant="h6"
component="div" component="div"
sx={{ sx={{
mb: 1, mb: 1.5,
fontWeight: 600, fontWeight: 600,
color: '#fff', lineHeight: '1.4',
lineHeight: '1.3',
// Allow text to wrap
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
display: '-webkit-box', display: '-webkit-box',
WebkitLineClamp: 2, WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
minHeight: '2.8em',
}} }}
> >
{props.displayName} {props.displayName}
</Typography> </Typography>
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
mb: 1.5, gap: 2,
mb: 2,
flexWrap: 'wrap',
}}> }}>
<Explore sx={{ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
mr: 0.75, <Explore sx={{ fontSize: '1rem', color: '#6366f1' }} />
mt: 0.25, <Typography variant="body2" color="text.secondary" fontSize="0.875rem">
color: 'text.secondary', {getGameName(props.gameID)}
fontSize: '0.9rem', </Typography>
flexShrink: 0, </Box>
}} /> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography <Person2 sx={{ fontSize: '1rem', color: '#8b5cf6' }} />
variant="body2" {props.type === 'mapfix' && usernameLoading ? (
color="text.secondary" <Skeleton variant="text" width={80} />
sx={{ ) : (
fontWeight: 500, <Typography
// Allow text to wrap variant="body2"
overflow: 'hidden', color="text.secondary"
textOverflow: 'ellipsis', fontSize="0.875rem"
display: '-webkit-box', sx={{
WebkitLineClamp: 2, overflow: 'hidden',
WebkitBoxOrient: 'vertical', textOverflow: 'ellipsis',
lineHeight: '1.2', whiteSpace: 'nowrap',
wordBreak: 'break-word', }}
}} >
> {props.type === 'mapfix' && username ? `@${username}` : props.author}
{props.gameID === 1 ? 'Bhop' : props.gameID === 2 ? 'Surf' : props.gameID === 5 ? 'Fly Trials' : props.gameID === 4 ? 'Deathrun' : 'Unknown'} </Typography>
</Typography> )}
</Box> </Box>
<Box sx={{
display: 'flex',
mb: 1.5,
}}>
<Person2 sx={{
mr: 0.75,
mt: 0.25,
color: 'text.secondary',
fontSize: '0.9rem',
flexShrink: 0,
}} />
<Typography
variant="body2"
color="text.secondary"
sx={{
fontWeight: 500,
// Allow text to wrap
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
lineHeight: '1.2',
wordBreak: 'break-word',
}}
>
{props.author}
</Typography>
</Box> </Box>
</Box> </Box>
<Box> <Box>
<Divider sx={{ my: 1.5 }} /> <Divider sx={{ my: 1.5 }} />
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Skeleton
variant="circular"
width={28}
height={28}
sx={{
position: 'absolute',
opacity: userLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
/>
<Avatar <Avatar
src={`/thumbnails/user/${props.authorId}`} src={userThumbnail || undefined}
alt={props.author} alt={props.author}
sx={{ sx={{
width: 24, width: 28,
height: 24, height: 28,
border: '1px solid rgba(255, 255, 255, 0.1)', opacity: userLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}} }}
/> />
<Typography <Typography
variant="caption" variant="caption"
color="text.secondary"
sx={{ sx={{
ml: 1,
color: 'text.secondary',
fontWeight: 500,
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}} }}
> >
{/*In the future author should be the username of the submitter not the info from the map*/} {new Date(props.created * 1000).toLocaleDateString('en-US', {
{props.author} - {new Date(props.created * 1000).toLocaleDateString('en-US', { month: 'short',
year: 'numeric', day: 'numeric',
month: 'long', year: 'numeric'
day: 'numeric'
})} })}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
</CardContent> </CardContent>
</CardActionArea> </CardActionArea>
</Card> </Card>
</Box>
</Grid>
) )
} }

View File

@@ -1,13 +1,16 @@
import React from 'react'; import React, { useState } from 'react';
import { Button, Stack } from '@mui/material'; import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, Typography, Box } from '@mui/material';
import {MapfixInfo } from "@/app/ts/Mapfix"; import {MapfixInfo } from "@/app/ts/Mapfix";
import {hasRole, Roles, RolesConstants} from "@/app/ts/Roles"; import {hasRole, Roles, RolesConstants} from "@/app/ts/Roles";
import {SubmissionInfo} from "@/app/ts/Submission"; import {SubmissionInfo} from "@/app/ts/Submission";
import {Status, StatusMatches} from "@/app/ts/Status"; import {Status, StatusMatches} from "@/app/ts/Status";
interface ReviewAction { interface ReviewAction {
name: string, name: string;
action: string, action: string;
confirmTitle?: string;
confirmMessage?: string;
requiresConfirmation: boolean;
} }
interface ReviewButtonsProps { interface ReviewButtonsProps {
@@ -19,20 +22,102 @@ interface ReviewButtonsProps {
} }
const ReviewActions = { const ReviewActions = {
Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction, Submit: {
AdminSubmit: {name:"Admin Submit",action:"trigger-submit"} as ReviewAction, name: "Submit for Review",
SubmitUnchecked: {name:"Submit Unchecked", action:"trigger-submit-unchecked"} as ReviewAction, action: "trigger-submit",
ResetSubmitting: {name:"Reset Submitting",action:"reset-submitting"} as ReviewAction, confirmTitle: "Submit for Review",
Revoke: {name:"Revoke",action:"revoke"} as ReviewAction, confirmMessage: "Are you ready to submit this for review? The model version is locked in once submitted, but you can revoke it later if needed.",
Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction, requiresConfirmation: true
Reject: {name:"Reject",action:"reject"} as ReviewAction, } as ReviewAction,
Validate: {name:"Validate",action:"retry-validate"} as ReviewAction, AdminSubmit: {
ResetValidating: {name:"Reset Validating",action:"reset-validating"} as ReviewAction, name: "Submit on Behalf of User",
RequestChanges: {name:"Request Changes",action:"request-changes"} as ReviewAction, action: "trigger-submit",
Upload: {name:"Upload",action:"trigger-upload"} as ReviewAction, confirmTitle: "Admin Submit",
ResetUploading: {name:"Reset Uploading",action:"reset-uploading"} as ReviewAction, confirmMessage: "This will submit the work as if the original user did it. Continue?",
Release: {name:"Release",action:"trigger-release"} as ReviewAction, requiresConfirmation: true
ResetReleasing: {name:"Reset Releasing",action:"reset-releasing"} as ReviewAction, } as ReviewAction,
SubmitUnchecked: {
name: "Approve Without Validation",
action: "trigger-submit-unchecked",
confirmTitle: "Skip Validation",
confirmMessage: "This will approve without running validation checks. Only use this if you're certain the work is correct.",
requiresConfirmation: true
} as ReviewAction,
ResetSubmitting: {
name: "Reset Submit Process",
action: "reset-submitting",
confirmTitle: "Reset Submit",
confirmMessage: "This will force-cancel the submission process and return to 'Under Construction' status. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
requiresConfirmation: true
} as ReviewAction,
Revoke: {
name: "Revoke",
action: "revoke",
confirmTitle: "Revoke",
confirmMessage: "This will withdraw from review and return to 'Under Construction' status.",
requiresConfirmation: true
} as ReviewAction,
Accept: {
name: "Accept & Validate",
action: "trigger-validate",
confirmTitle: "Accept",
confirmMessage: "This will accept and trigger validation. The work will proceed to the next stage.",
requiresConfirmation: true
} as ReviewAction,
Reject: {
name: "Reject",
action: "reject",
confirmTitle: "Reject",
confirmMessage: "This will permanently reject. The user will need to create a new one. Are you sure?",
requiresConfirmation: true
} as ReviewAction,
Validate: {
name: "Run Validation",
action: "retry-validate",
requiresConfirmation: false
} as ReviewAction,
ResetValidating: {
name: "Reset Validation Process",
action: "reset-validating",
confirmTitle: "Reset Validation",
confirmMessage: "This will force-abort the validation process so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
requiresConfirmation: true
} as ReviewAction,
RequestChanges: {
name: "Request Changes",
action: "request-changes",
confirmTitle: "Request Changes",
confirmMessage: "Request that the submitter make changes. Make sure you've explained which changes are requested in a comment.",
requiresConfirmation: true
} as ReviewAction,
Upload: {
name: "Upload to Roblox",
action: "trigger-upload",
confirmTitle: "Upload to Roblox Group",
confirmMessage: "This will upload the validated work to the Roblox group. Continue?",
requiresConfirmation: true
} as ReviewAction,
ResetUploading: {
name: "Reset Upload Process",
action: "reset-uploading",
confirmTitle: "Reset Upload",
confirmMessage: "This will force-abort the upload to Roblox so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
requiresConfirmation: true
} as ReviewAction,
Release: {
name: "Release to Game",
action: "trigger-release",
confirmTitle: "Release to Game",
confirmMessage: "This will make the work available in game. This is the final step!",
requiresConfirmation: true
} as ReviewAction,
ResetReleasing: {
name: "Reset Release Process",
action: "reset-releasing",
confirmTitle: "Reset Release",
confirmMessage: "This will force-abort the release to the game so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
requiresConfirmation: true
} as ReviewAction,
} }
const ReviewButtons: React.FC<ReviewButtonsProps> = ({ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
@@ -42,16 +127,46 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
roles, roles,
type, type,
}) => { }) => {
const getVisibleButtons = () => { const [confirmDialog, setConfirmDialog] = useState<{
if (!item || userId === null) return []; open: boolean;
action: ReviewAction | null;
}>({ open: false, action: null });
const handleButtonClick = (action: ReviewAction) => {
if (action.requiresConfirmation) {
setConfirmDialog({ open: true, action });
} else {
onClick(action.action, item.ID);
}
};
const handleConfirm = () => {
if (confirmDialog.action) {
onClick(confirmDialog.action.action, item.ID);
}
setConfirmDialog({ open: false, action: null });
};
const handleCancel = () => {
setConfirmDialog({ open: false, action: null });
};
const getVisibleButtons = () => {
if (!item || userId === null) return { primary: [], secondary: [], submitter: [], reviewer: [], admin: [] };
// Define a type for the button
type ReviewButton = { type ReviewButton = {
action: ReviewAction; action: ReviewAction;
color: "primary" | "error" | "success" | "info" | "warning"; color: "primary" | "error" | "success" | "info" | "warning";
variant?: "contained" | "outlined";
isPrimary?: boolean;
}; };
const buttons: ReviewButton[] = []; const primaryButtons: ReviewButton[] = [];
const secondaryButtons: ReviewButton[] = [];
const submitterButtons: ReviewButton[] = [];
const reviewerButtons: ReviewButton[] = [];
const adminButtons: ReviewButton[] = [];
const is_submitter = userId === item.Submitter; const is_submitter = userId === item.Submitter;
const status = item.StatusID; const status = item.StatusID;
@@ -59,133 +174,215 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
const uploadRole = type === "submission" ? RolesConstants.SubmissionUpload : RolesConstants.MapfixUpload; const uploadRole = type === "submission" ? RolesConstants.SubmissionUpload : RolesConstants.MapfixUpload;
const releaseRole = type === "submission" ? RolesConstants.SubmissionRelease : RolesConstants.MapfixRelease; const releaseRole = type === "submission" ? RolesConstants.SubmissionRelease : RolesConstants.MapfixRelease;
// Submitter actions
if (is_submitter) { if (is_submitter) {
if (StatusMatches(status, [Status.UnderConstruction, Status.ChangesRequested])) { if (StatusMatches(status, [Status.UnderConstruction, Status.ChangesRequested])) {
buttons.push({ submitterButtons.push({
action: ReviewActions.Submit, action: ReviewActions.Submit,
color: "primary" color: "success"
}); });
} }
if (StatusMatches(status, [Status.Submitted, Status.ChangesRequested])) { if (StatusMatches(status, [Status.Submitted, Status.ChangesRequested])) {
buttons.push({ submitterButtons.push({
action: ReviewActions.Revoke, action: ReviewActions.Revoke,
color: "error" color: "warning",
variant: "outlined"
}); });
} }
if (status === Status.Submitting) { if (status === Status.Submitting) {
buttons.push({ adminButtons.push({
action: ReviewActions.ResetSubmitting, action: ReviewActions.ResetSubmitting,
color: "warning" color: "error",
variant: "outlined"
}); });
} }
} }
// Buttons for review role // Reviewer actions
if (hasRole(roles, reviewRole)) { if (hasRole(roles, reviewRole)) {
if (status === Status.Submitted && !is_submitter) { if (status === Status.Submitted && !is_submitter) {
buttons.push( reviewerButtons.push({
{ action: ReviewActions.Accept,
action: ReviewActions.Accept, color: "success"
color: "success" });
}, reviewerButtons.push({
{ action: ReviewActions.Reject,
action: ReviewActions.Reject, color: "error",
color: "error" variant: "outlined"
} });
);
} }
if (status === Status.AcceptedUnvalidated) { if (status === Status.AcceptedUnvalidated) {
buttons.push({ reviewerButtons.push({
action: ReviewActions.Validate, action: ReviewActions.Validate,
color: "info" color: "primary"
}); });
} }
if (status === Status.Validating) { if (status === Status.Validating) {
buttons.push({ adminButtons.push({
action: ReviewActions.ResetValidating, action: ReviewActions.ResetValidating,
color: "warning" color: "error",
variant: "outlined"
}); });
} }
if (StatusMatches(status, [Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) { if (StatusMatches(status, [Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) {
buttons.push({ reviewerButtons.push({
action: ReviewActions.RequestChanges, action: ReviewActions.RequestChanges,
color: "warning" color: "warning",
variant: "outlined"
}); });
} }
if (status === Status.ChangesRequested) { if (status === Status.ChangesRequested) {
buttons.push({ adminButtons.push({
action: ReviewActions.SubmitUnchecked, action: ReviewActions.SubmitUnchecked,
color: "warning" color: "warning",
variant: "outlined"
}); });
// button only exists for submissions
// submitter has normal submit button
if (type === "submission" && !is_submitter) { if (type === "submission" && !is_submitter) {
buttons.push({ adminButtons.push({
action: ReviewActions.AdminSubmit, action: ReviewActions.AdminSubmit,
color: "primary" color: "info",
variant: "outlined"
}); });
} }
} }
} }
// Buttons for upload role // Upload role actions
if (hasRole(roles, uploadRole)) { if (hasRole(roles, uploadRole)) {
if (status === Status.Validated) { if (status === Status.Validated) {
buttons.push({ reviewerButtons.push({
action: ReviewActions.Upload, action: ReviewActions.Upload,
color: "success" color: "success"
}); });
} }
if (status === Status.Uploading) { if (status === Status.Uploading) {
buttons.push({ adminButtons.push({
action: ReviewActions.ResetUploading, action: ReviewActions.ResetUploading,
color: "warning" color: "error",
variant: "outlined"
}); });
} }
} }
// Buttons for release role // Release role actions
if (hasRole(roles, releaseRole)) { if (hasRole(roles, releaseRole)) {
// submissions do not have a release button
if (type === "mapfix" && status === Status.Uploaded) { if (type === "mapfix" && status === Status.Uploaded) {
buttons.push({ reviewerButtons.push({
action: ReviewActions.Release, action: ReviewActions.Release,
color: "success" color: "success"
}); });
} }
if (status === Status.Releasing) { if (status === Status.Releasing) {
buttons.push({ adminButtons.push({
action: ReviewActions.ResetReleasing, action: ReviewActions.ResetReleasing,
color: "warning" color: "error",
variant: "outlined"
}); });
} }
} }
return buttons; return {
primary: primaryButtons,
secondary: secondaryButtons,
submitter: submitterButtons,
reviewer: reviewerButtons,
admin: adminButtons
};
};
const buttons = getVisibleButtons();
const hasAnyButtons = buttons.submitter.length > 0 || buttons.reviewer.length > 0 || buttons.admin.length > 0;
if (!hasAnyButtons) return null;
const ActionCard = ({ title, actions, isFirst = false }: { title: string; actions: any[]; isFirst?: boolean }) => {
if (actions.length === 0) return null;
return (
<Box sx={{ mt: isFirst ? 0 : 3 }}>
<Typography
variant="caption"
fontWeight={600}
color="text.secondary"
sx={{
textTransform: 'uppercase',
letterSpacing: '0.5px',
mb: 1.5,
display: 'block'
}}
>
{title}
</Typography>
<Stack spacing={1}>
{actions.map((button, index) => (
<Button
key={index}
variant="contained"
color={button.color}
fullWidth
size="large"
onClick={() => handleButtonClick(button.action)}
sx={{
textTransform: 'none',
fontSize: '1rem',
fontWeight: 600,
py: 1.5
}}
>
{button.action.name}
</Button>
))}
</Stack>
</Box>
);
}; };
return ( return (
<Stack spacing={2} sx={{ mb: 3 }}> <>
{getVisibleButtons().map((button, index) => ( <Box sx={{ mb: 3 }}>
<Button <ActionCard title="Your Actions" actions={buttons.submitter} isFirst={true} />
key={index} <ActionCard title="Review Actions" actions={buttons.reviewer} isFirst={buttons.submitter.length === 0} />
variant="contained" <ActionCard title="Admin Actions" actions={buttons.admin} isFirst={buttons.submitter.length === 0 && buttons.reviewer.length === 0} />
color={button.color} </Box>
fullWidth
onClick={() => onClick(button.action.action, item.ID)} {/* Confirmation Dialog */}
> <Dialog
{button.action.name} open={confirmDialog.open}
</Button> onClose={handleCancel}
))} maxWidth="xs"
</Stack> fullWidth
>
<DialogTitle sx={{ pb: 1 }}>
{confirmDialog.action?.confirmTitle || confirmDialog.action?.name}
</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary">
{confirmDialog.action?.confirmMessage || "Are you sure you want to proceed?"}
</Typography>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={handleCancel} color="inherit">
Cancel
</Button>
<Button
onClick={handleConfirm}
variant="contained"
color="primary"
autoFocus
>
Confirm
</Button>
</DialogActions>
</Dialog>
</>
); );
}; };

View File

@@ -1,8 +1,15 @@
import { Paper, Grid, Typography } from "@mui/material"; import { Paper, Grid, Typography, TextField, IconButton, Box } from "@mui/material";
import { ReviewItemHeader } from "./ReviewItemHeader"; import { ReviewItemHeader } from "./ReviewItemHeader";
import { CopyableField } from "@/app/_components/review/CopyableField"; import { CopyableField } from "@/app/_components/review/CopyableField";
import WorkflowStepper from "./WorkflowStepper";
import { SubmissionInfo } from "@/app/ts/Submission"; import { SubmissionInfo } from "@/app/ts/Submission";
import { MapfixInfo } from "@/app/ts/Mapfix"; import { MapfixInfo } from "@/app/ts/Mapfix";
import { getGameName } from "@/app/utils/games";
import { useState } from "react";
import EditIcon from '@mui/icons-material/Edit';
import SaveIcon from '@mui/icons-material/Save';
import CloseIcon from '@mui/icons-material/Close';
import { Status, StatusMatches } from "@/app/ts/Status";
// Define a field configuration for specific types // Define a field configuration for specific types
interface FieldConfig { interface FieldConfig {
@@ -16,12 +23,24 @@ type ReviewItemType = SubmissionInfo | MapfixInfo;
interface ReviewItemProps { interface ReviewItemProps {
item: ReviewItemType; item: ReviewItemType;
handleCopyValue: (value: string) => void; handleCopyValue: (value: string) => void;
currentUserId?: number;
userId?: number | null;
onDescriptionUpdate?: () => Promise<void>;
showSnackbar?: (message: string, severity?: 'success' | 'error' | 'info' | 'warning') => void;
} }
export function ReviewItem({ export function ReviewItem({
item, item,
handleCopyValue handleCopyValue,
currentUserId,
userId,
onDescriptionUpdate,
showSnackbar
}: ReviewItemProps) { }: ReviewItemProps) {
const [isEditingDescription, setIsEditingDescription] = useState(false);
const [editedDescription, setEditedDescription] = useState("");
const [isSaving, setIsSaving] = useState(false);
// Type guard to check if item is valid // Type guard to check if item is valid
if (!item) return null; if (!item) return null;
@@ -29,6 +48,57 @@ export function ReviewItem({
const isSubmission = 'UploadedAssetID' in item; const isSubmission = 'UploadedAssetID' in item;
const isMapfix = 'TargetAssetID' in item; const isMapfix = 'TargetAssetID' in item;
// Check if current user is the submitter
const isSubmitter = userId !== null && userId === item.Submitter;
// Check if description can be edited (only in ChangesRequested or UnderConstruction status)
const canEditDescription = isSubmitter && isMapfix && StatusMatches(item.StatusID, [Status.ChangesRequested, Status.UnderConstruction]);
const handleEditClick = () => {
setEditedDescription(isMapfix ? (item.Description || "") : "");
setIsEditingDescription(true);
};
const handleCancelEdit = () => {
setIsEditingDescription(false);
setEditedDescription("");
};
const handleSaveDescription = async () => {
if (!isMapfix) return;
setIsSaving(true);
try {
const response = await fetch(`/v1/mapfixes/${item.ID}/description`, {
method: 'PATCH',
headers: {
'Content-Type': 'text/plain',
},
body: editedDescription,
});
if (!response.ok) {
throw new Error(`Failed to update description: ${response.status}`);
}
setIsEditingDescription(false);
if (showSnackbar) {
showSnackbar("Description updated successfully", "success");
}
if (onDescriptionUpdate) {
await onDescriptionUpdate();
}
} catch (error) {
console.error("Error updating description:", error);
const errorMessage = error instanceof Error ? error.message : "Failed to update description";
if (showSnackbar) {
showSnackbar(errorMessage, "error");
}
} finally {
setIsSaving(false);
}
};
// Define static fields based on item type // Define static fields based on item type
let fields: FieldConfig[] = []; let fields: FieldConfig[] = [];
if (isSubmission) { if (isSubmission) {
@@ -46,17 +116,18 @@ export function ReviewItem({
} }
return ( return (
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4 }}> <>
<ReviewItemHeader <Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4 }}>
displayName={item.DisplayName} <ReviewItemHeader
assetId={isMapfix ? item.TargetAssetID : undefined} displayName={item.DisplayName}
statusId={item.StatusID} assetId={isMapfix ? item.TargetAssetID : undefined}
creator={item.Creator} statusId={item.StatusID}
submitterId={item.Submitter} creator={item.Creator}
/> submitterId={item.Submitter}
/>
{/* Item Details */} {/* Item Details */}
<Grid container spacing={2} sx={{ mt: 2 }}> <Grid container spacing={2} sx={{ mt: 2 }}>
{fields.map((field) => { {fields.map((field) => {
const fieldValue = (item as never)[field.key]; const fieldValue = (item as never)[field.key];
const displayValue = fieldValue === 0 || fieldValue == null ? 'N/A' : fieldValue; const displayValue = fieldValue === 0 || fieldValue == null ? 'N/A' : fieldValue;
@@ -74,19 +145,83 @@ export function ReviewItem({
</Grid> </Grid>
); );
})} })}
</Grid> <Grid size={{ xs: 12, sm: 6}}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
{/* Description Section */} Game
{isMapfix && item.Description && (
<div style={{ marginTop: 24 }}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
Description
</Typography> </Typography>
<Typography variant="body1"> <Typography variant="body1">
{item.Description} {getGameName(item.GameID)}
</Typography> </Typography>
</div> </Grid>
)} </Grid>
</Paper>
{/* Description Section */}
{isMapfix && (
<div style={{ marginTop: 24 }}>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
<Typography variant="subtitle1" fontWeight="bold">
Description
</Typography>
{canEditDescription && !isEditingDescription && (
<IconButton
size="small"
onClick={handleEditClick}
sx={{ ml: 1 }}
aria-label="edit description"
>
<EditIcon fontSize="small" />
</IconButton>
)}
</Box>
{isEditingDescription ? (
<Box>
<TextField
fullWidth
multiline
rows={4}
value={editedDescription}
onChange={(e) => setEditedDescription(e.target.value)}
placeholder="Describe the changes made in this mapfix"
slotProps={{ htmlInput: { maxLength: 256 } }}
helperText={`${editedDescription.length}/256 characters`}
disabled={isSaving}
/>
<Box display="flex" gap={1} mt={1}>
<IconButton
color="primary"
onClick={handleSaveDescription}
disabled={isSaving}
aria-label="save description"
>
<SaveIcon />
</IconButton>
<IconButton
onClick={handleCancelEdit}
disabled={isSaving}
aria-label="cancel edit"
>
<CloseIcon />
</IconButton>
</Box>
</Box>
) : (
<Typography variant="body1">
{item.Description || "No description provided"}
</Typography>
)}
</div>
)}
</Paper>
{/* Workflow Progress Indicator */}
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4, display: { xs: 'none', md: 'block' } }}>
<WorkflowStepper
currentStatus={item.StatusID}
type={isMapfix ? 'mapfix' : 'submission'}
submitterId={item.Submitter}
currentUserId={currentUserId}
/>
</Paper>
</>
); );
} }

View File

@@ -1,43 +1,44 @@
import {Typography, Box, Avatar, keyframes} from "@mui/material"; import {Typography, Box, Avatar, keyframes, Skeleton} from "@mui/material";
import { StatusChip } from "@/app/_components/statusChip"; import { StatusChip } from "@/app/_components/statusChip";
import { SubmissionStatus } from "@/app/ts/Submission"; import { SubmissionStatus } from "@/app/ts/Submission";
import { MapfixStatus } from "@/app/ts/Mapfix"; import { MapfixStatus } from "@/app/ts/Mapfix";
import {Status, StatusMatches} from "@/app/ts/Status"; import {Status, StatusMatches} from "@/app/ts/Status";
import { useState, useEffect } from "react"; import { Link } from "react-router-dom";
import Link from "next/link";
import LaunchIcon from '@mui/icons-material/Launch'; import LaunchIcon from '@mui/icons-material/Launch';
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
import { useUsername } from "@/app/hooks/useUsername";
function SubmitterName({ submitterId }: { submitterId: number }) { function SubmitterName({ submitterId }: { submitterId: number }) {
const [name, setName] = useState<string | null>(null); const { username, isLoading } = useUsername(submitterId);
const [loading, setLoading] = useState(true);
useEffect(() => { const displayName = username ? `@${username}` : String(submitterId);
if (!submitterId) return;
const fetchUserName = async () => {
try {
setLoading(true);
const response = await fetch(`/proxy/users/${submitterId}`);
if (!response.ok) throw new Error('Failed to fetch user');
const data = await response.json();
setName(`@${data.name}`);
} catch {
setName(String(submitterId));
} finally {
setLoading(false);
}
};
fetchUserName();
}, [submitterId]);
if (loading) return <Typography variant="body1">Loading...</Typography>; return <a href={`https://www.roblox.com/users/${submitterId}/profile`} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none', color: 'inherit' }}>
return <Link href={`https://www.roblox.com/users/${submitterId}/profile`} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none', color: 'inherit' }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, '&:hover': { textDecoration: 'underline' }, position: 'relative' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, '&:hover': { textDecoration: 'underline' } }}> <Skeleton
<Typography> variant="text"
{name || submitterId} width={80}
</Typography> sx={{
<LaunchIcon sx={{ fontSize: '1rem', color: 'text.secondary' }} /> opacity: isLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
position: isLoading ? 'relative' : 'absolute',
}}
animation="wave"
/>
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}>
<Typography>
{displayName}
</Typography>
<LaunchIcon sx={{ fontSize: '1rem', color: 'text.secondary' }} />
</Box>
</Box> </Box>
</Link> </a>
} }
interface ReviewItemHeaderProps { interface ReviewItemHeaderProps {
@@ -49,7 +50,8 @@ interface ReviewItemHeaderProps {
} }
export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, submitterId }: ReviewItemHeaderProps) => { export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, submitterId }: ReviewItemHeaderProps) => {
const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting]); const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting, Status.Releasing]);
const { thumbnailUrl, isLoading } = useUserThumbnail(submitterId, '150x150');
const pulse = keyframes` const pulse = keyframes`
0%, 100% { opacity: 0.2; transform: scale(0.8); } 0%, 100% { opacity: 0.2; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1); } 50% { opacity: 1; transform: scale(1); }
@@ -59,7 +61,7 @@ export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, subm
<> <>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
{assetId != null ? ( {assetId != null ? (
<Link href={`/maps/${assetId}`} passHref legacyBehavior> <Link to={`/maps/${assetId}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<Box sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} title="View related map"> <Box sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} title="View related map">
<Typography <Typography
variant="h4" variant="h4"
@@ -111,10 +113,28 @@ export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, subm
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Avatar <Box sx={{ position: 'relative', mr: 1, width: 24, height: 24 }}>
src={`/thumbnails/user/${submitterId}`} <Skeleton
sx={{ mr: 1, width: 24, height: 24 }} variant="circular"
/> sx={{
position: 'absolute',
width: 24,
height: 24,
opacity: isLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
animation="wave"
/>
<Avatar
src={thumbnailUrl || undefined}
sx={{
width: 24,
height: 24,
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}
/>
</Box>
<SubmitterName submitterId={submitterId} /> <SubmitterName submitterId={submitterId} />
</Box> </Box>
</> </>

View File

@@ -0,0 +1,315 @@
import React from 'react';
import { Stepper, Step, StepLabel, Box, StepConnector, stepConnectorClasses, StepIconProps, styled, keyframes, Typography, Paper } from '@mui/material';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import CancelIcon from '@mui/icons-material/Cancel';
import PendingIcon from '@mui/icons-material/Pending';
import WarningIcon from '@mui/icons-material/Warning';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import { Status } from '@/app/ts/Status';
const pulse = keyframes`
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
`;
interface WorkflowStepperProps {
currentStatus: number;
type: 'submission' | 'mapfix';
submitterId?: number;
currentUserId?: number;
}
// Define the workflow steps
interface WorkflowStep {
label: string;
statuses: number[];
description?: string;
}
// Transitional states that show as "in progress"
const transitionalStates = [
Status.Submitting,
Status.Validating,
Status.Uploading,
Status.Releasing
];
const submissionWorkflow: WorkflowStep[] = [
{
label: 'Draft',
statuses: [Status.UnderConstruction, Status.ChangesRequested],
description: 'Creating or revising'
},
{
label: 'Submitted',
statuses: [Status.Submitting, Status.Submitted],
description: 'Awaiting review'
},
{
label: 'Accepted',
statuses: [Status.AcceptedUnvalidated],
description: 'Script review pending'
},
{
label: 'Validated',
statuses: [Status.Validating, Status.Validated],
description: 'Scripts approved'
},
{
label: 'Uploaded',
statuses: [Status.Uploading, Status.Uploaded],
description: 'Published to Roblox group'
},
{
label: 'Released',
statuses: [Status.Releasing, Status.Release],
description: 'Live in-game'
}
];
const mapfixWorkflow: WorkflowStep[] = [
{
label: 'Draft',
statuses: [Status.UnderConstruction, Status.ChangesRequested],
description: 'Creating or revising'
},
{
label: 'Submitted',
statuses: [Status.Submitting, Status.Submitted],
description: 'Awaiting review'
},
{
label: 'Accepted',
statuses: [Status.AcceptedUnvalidated],
description: 'Script review pending'
},
{
label: 'Validated',
statuses: [Status.Validating, Status.Validated],
description: 'Scripts approved'
},
{
label: 'Uploaded',
statuses: [Status.Uploading, Status.Uploaded],
description: 'Published to Roblox group'
},
{
label: 'Released',
statuses: [Status.Releasing, Status.Release],
description: 'Live in-game'
}
];
const CustomConnector = styled(StepConnector)(({ theme }) => ({
[`&.${stepConnectorClasses.alternativeLabel}`]: {
top: 10,
left: 'calc(-50% + 16px)',
right: 'calc(50% + 16px)',
},
[`&.${stepConnectorClasses.active}`]: {
[`& .${stepConnectorClasses.line}`]: {
borderColor: theme.palette.primary.main,
},
},
[`&.${stepConnectorClasses.completed}`]: {
[`& .${stepConnectorClasses.line}`]: {
borderColor: theme.palette.success.main,
},
},
[`& .${stepConnectorClasses.line}`]: {
borderColor: theme.palette.mode === 'dark' ? theme.palette.grey[800] : '#eaeaf0',
borderTopWidth: 3,
borderRadius: 1,
transition: 'border-color 0.4s ease-in-out',
},
}));
const CustomStepIcon = (props: StepIconProps & { isRejected?: boolean; isChangesRequested?: boolean }) => {
const { active, completed, className, isRejected, isChangesRequested } = props;
const iconStyle = {
transition: 'color 0.4s ease-in-out, opacity 0.3s ease-in-out, transform 0.3s ease-in-out',
};
if (isRejected) {
return <CancelIcon className={className} sx={{ ...iconStyle, color: 'error.main' }} />;
}
if (completed) {
return <CheckCircleIcon className={className} sx={{ ...iconStyle, color: 'success.main' }} />;
}
if (active && isChangesRequested) {
return <WarningIcon className={className} sx={{ ...iconStyle, color: 'warning.main' }} />;
}
if (active) {
return <PendingIcon className={className} sx={{ ...iconStyle, color: 'primary.main', animation: `${pulse} 2s ease-in-out infinite` }} />;
}
return (
<Box
className={className}
sx={{
width: 24,
height: 24,
borderRadius: '50%',
border: 2,
borderColor: 'grey.400',
backgroundColor: 'background.paper',
transition: 'all 0.4s ease-in-out',
}}
/>
);
};
const WorkflowStepper: React.FC<WorkflowStepperProps> = ({ currentStatus, type, submitterId, currentUserId }) => {
const workflow = type === 'mapfix' ? mapfixWorkflow : submissionWorkflow;
// Check if rejected or released
const isRejected = currentStatus === Status.Rejected;
const isReleased = currentStatus === Status.Release || currentStatus === Status.Releasing;
const isChangesRequested = currentStatus === Status.ChangesRequested;
const isUnderConstruction = currentStatus === Status.UnderConstruction;
// Find the active step
const activeStep = workflow.findIndex(step =>
step.statuses.includes(currentStatus)
);
// Determine nudge message
const getNudgeContent = () => {
if (isUnderConstruction) {
return {
icon: InfoOutlinedIcon,
title: 'Not Yet Submitted',
message: 'Your submission has been created but has not been submitted. Click "Submit" to submit it.',
color: '#2196f3',
bgColor: 'rgba(33, 150, 243, 0.08)'
};
}
if (isChangesRequested) {
return {
icon: WarningIcon,
title: 'Changes Requested',
message: 'Review comments and audit events, make modifications, and submit again.',
color: '#ff9800',
bgColor: 'rgba(255, 152, 0, 0.08)'
};
}
return null;
};
const nudge = getNudgeContent();
// Only show nudge if current user is the submitter
const isSubmitter = submitterId !== undefined && currentUserId !== undefined && submitterId === currentUserId;
const shouldShowNudge = nudge && isSubmitter;
// If rejected, show all steps as incomplete with error state
if (isRejected) {
return (
<Box sx={{ width: '100%' }}>
<Stepper activeStep={-1} alternativeLabel connector={<CustomConnector />}>
{workflow.map((step) => (
<Step key={step.label} completed={false}>
<StepLabel
StepIconComponent={(props) => <CustomStepIcon {...props} isRejected={true} />}
error={true}
>
<Box sx={{ fontSize: '0.875rem', fontWeight: 500 }}>
{step.label}
</Box>
<Box sx={{ fontSize: '0.75rem', color: 'error.main', mt: 0.5 }}>
Rejected
</Box>
</StepLabel>
</Step>
))}
</Stepper>
</Box>
);
}
return (
<Box sx={{ width: '100%' }}>
<Stepper activeStep={activeStep} alternativeLabel connector={<CustomConnector />}>
{workflow.map((step, index) => {
const stepIncludesCurrentStatus = index === activeStep;
const isTransitional = transitionalStates.includes(currentStatus);
// Show as active if in a transitional state OR if changes requested
const isActive = stepIncludesCurrentStatus && (isTransitional || isChangesRequested);
const isCompleted = isReleased
? true
: index < activeStep || (stepIncludesCurrentStatus && !isTransitional && !isChangesRequested);
return (
<Step key={step.label} completed={isCompleted}>
<StepLabel
StepIconComponent={(props) => <CustomStepIcon {...props} isChangesRequested={stepIncludesCurrentStatus && isChangesRequested} />}
>
<Box sx={{
fontSize: '0.875rem',
fontWeight: isReleased ? 500 : (isActive ? 600 : 500),
transition: 'all 0.4s ease-in-out'
}}>
{step.label}
</Box>
{step.description && (
<Box
sx={{
fontSize: '0.75rem',
color: isReleased ? 'text.secondary' : (stepIncludesCurrentStatus && isChangesRequested ? 'warning.main' : (isActive ? 'primary.main' : 'text.secondary')),
mt: 0.5,
transition: 'color 0.4s ease-in-out'
}}
>
{step.description}
</Box>
)}
</StepLabel>
</Step>
);
})}
</Stepper>
{/* Action Nudge */}
{shouldShowNudge && (
<Paper
elevation={0}
sx={{
mt: 3,
p: 2,
borderRadius: 2,
borderLeft: 4,
borderColor: nudge.color,
backgroundColor: nudge.bgColor,
display: 'flex',
gap: 1.5,
alignItems: 'flex-start'
}}
>
<Box sx={{ color: nudge.color, display: 'flex', alignItems: 'center', pt: 0.25 }}>
<nudge.icon sx={{ fontSize: 24 }} />
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={600} sx={{ color: nudge.color, mb: 0.5 }}>
{nudge.title}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary', fontSize: '0.875rem' }}>
{nudge.message}
</Typography>
</Box>
</Paper>
)}
</Box>
);
};
export default WorkflowStepper;

View File

@@ -1,8 +1,8 @@
import React, {JSX} from "react"; import {JSX} from "react";
import {Cancel, CheckCircle, Pending} from "@mui/icons-material"; import {Cancel, CheckCircle, Pending} from "@mui/icons-material";
import {Chip} from "@mui/material"; import {Chip} from "@mui/material";
export const StatusChip = ({status}: { status: number }) => { export const StatusChip = ({status}: { status: number }): JSX.Element => {
let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default'; let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default';
let icon: JSX.Element = <Pending fontSize="small"/>; let icon: JSX.Element = <Pending fontSize="small"/>;
let label: string = 'Unknown'; let label: string = 'Unknown';
@@ -81,12 +81,6 @@ export const StatusChip = ({status}: { status: number }) => {
label={label} label={label}
color={color} color={color}
size="small" size="small"
sx={{
height: 24,
fontSize: '0.75rem',
fontWeight: 600,
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
}}
/> />
); );
}; };

View File

@@ -1,5 +1,3 @@
"use client"
import Header from "./header"; import Header from "./header";
export default function Webpage({children}: Readonly<{children?: React.ReactNode}>) { export default function Webpage({children}: Readonly<{children?: React.ReactNode}>) {

View File

@@ -1,4 +1,4 @@
@use "../../globals.scss"; @use "../globals.scss";
::placeholder { ::placeholder {
color: var(--placeholder-text) color: var(--placeholder-text)
@@ -47,8 +47,4 @@ header h1 {
form { form {
display: grid; display: grid;
gap: 25px; gap: 25px;
fieldset {
border: blue
}
} }

View File

@@ -1,5 +1,3 @@
"use client"
import { Button, TextField } from "@mui/material" import { Button, TextField } from "@mui/material"
import GameSelection from "./_game"; import GameSelection from "./_game";
@@ -8,7 +6,7 @@ import Webpage from "@/app/_components/webpage"
import React, { useState } from "react"; import React, { useState } from "react";
import {useTitle} from "@/app/hooks/useTitle"; import {useTitle} from "@/app/hooks/useTitle";
import "./(styles)/page.scss" import "./page.scss"
interface SubmissionPayload { interface SubmissionPayload {
AssetID: number; AssetID: number;
@@ -43,7 +41,7 @@ export default function SubmissionInfoPage() {
try { try {
// Send the POST request // Send the POST request
const response = await fetch("/api/submissions-admin", { const response = await fetch("/v1/submissions-admin", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),

View File

@@ -8,39 +8,23 @@ $form-label-fontsize: 1.3rem;
} }
:root { :root {
color-scheme: light dark; color-scheme: dark;
--header-height: 45px; --header-height: 45px;
--page: white; --page: rgb(15,15,15);
--header-grad-left: #363b40; --header-grad-left: #363b40;
--header-grad-right: #353a40; --header-grad-right: #353a40;
--header-button-left: white; --header-button-left: white;
--header-button-right: #b4b4b4; --header-button-right: #b4b4b4;
--header-button-hover: white; --header-button-hover: white;
--review-border: #c8c8c8; --review-border: rgb(50,50,50);
--text-color: #1e1e1e; --text-color: rgb(230,230,230);
--anchor-link-review: #008fd6; --anchor-link-review: #008fd6;
--window-header: #f5f5f5; --window-header: rgb(10,10,10);
--comment-highlighted: #ffffd7; --comment-highlighted: #ffffd7;
--comment-area: white; --comment-area: rgb(20,20,20);
--placeholder-text: rgb(150,150,150); --placeholder-text: rgb(80,80,80);
@media (prefers-color-scheme: dark) {
--page: rgb(15,15,15);
--header-grad-left: #363b40;
--header-grad-right: #353a40;
--header-button-left: white;
--header-button-right: #b4b4b4;
--header-button-hover: white;
--review-border: rgb(50,50,50);
--text-color: rgb(230,230,230);
--anchor-link-review: #008fd6;
--window-header: rgb(10,10,10);
--comment-highlighted: #ffffd7;
--comment-area: rgb(20,20,20);
--placeholder-text: rgb(80,80,80);
}
} }
body { body {

View File

@@ -40,11 +40,11 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
try { try {
const [reviewData, auditData] = await Promise.all([ const [reviewData, auditData] = await Promise.all([
fetch(`/api/${itemType}/${itemId}`).then(res => { fetch(`/v1/${itemType}/${itemId}`).then(res => {
if (!res.ok) throw new Error(`Failed to fetch ${itemType.slice(0, -1)}: ${res.status}`); if (!res.ok) throw new Error(`Failed to fetch ${itemType.slice(0, -1)}: ${res.status}`);
return res.json(); return res.json();
}), }),
fetch(`/api/${itemType}/${itemId}/audit-events?Page=1&Limit=100`).then(res => { fetch(`/v1/${itemType}/${itemId}/audit-events?Page=1&Limit=100`).then(res => {
if (!res.ok) throw new Error(`Failed to fetch audit events: ${res.status}`); if (!res.ok) throw new Error(`Failed to fetch audit events: ${res.status}`);
return res.json(); return res.json();
}) })
@@ -58,7 +58,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
} }
try { try {
const rolesResponse = await fetch("/api/session/roles"); const rolesResponse = await fetch("/v1/session/roles");
if (rolesResponse.ok) { if (rolesResponse.ok) {
const rolesData = await rolesResponse.json(); const rolesData = await rolesResponse.json();
setRoles(rolesData.Roles); setRoles(rolesData.Roles);
@@ -72,7 +72,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
} }
try { try {
const userResponse = await fetch("/api/session/user"); const userResponse = await fetch("/v1/session/user");
if (userResponse.ok) { if (userResponse.ok) {
const userData = await userResponse.json(); const userData = await userResponse.json();
setUser(userData.UserID); setUser(userData.UserID);
@@ -100,7 +100,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
useEffect(() => { useEffect(() => {
if (data) { if (data) {
if (StatusMatches(data.StatusID, [Status.Uploading, Status.Submitting, Status.Validating])) { if (StatusMatches(data.StatusID, [Status.Uploading, Status.Submitting, Status.Validating, Status.Releasing])) {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
fetchData(true); fetchData(true);
}, 5000); }, 5000);

View File

@@ -0,0 +1,216 @@
import { useEffect, useState } from 'react';
type ThumbnailSize = '150x150' | '420x420' | '768x432';
interface ThumbnailBatchResponse {
thumbnails: Record<string, string>;
}
// Batching queue
class ThumbnailBatcher {
private assetQueue: Map<number, Set<(url: string | null) => void>> = new Map();
private userQueue: Map<number, Set<(url: string | null) => void>> = new Map();
private assetTimeoutId: NodeJS.Timeout | null = null;
private userTimeoutId: NodeJS.Timeout | null = null;
private batchDelay = 50; // 50ms delay to collect requests
async fetchAssetBatch(ids: number[], size: ThumbnailSize): Promise<Record<number, string>> {
try {
const response = await fetch('/v1/thumbnails/assets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ assetIds: ids, size }),
});
if (!response.ok) {
throw new Error(`Failed to fetch thumbnails: ${response.statusText}`);
}
const data: ThumbnailBatchResponse = await response.json();
// Convert string keys back to numbers
const result: Record<number, string> = {};
Object.entries(data.thumbnails || {}).forEach(([key, value]) => {
result[parseInt(key, 10)] = value;
});
return result;
} catch (error) {
console.error('Error fetching asset thumbnails:', error);
return {};
}
}
async fetchUserBatch(ids: number[], size: ThumbnailSize): Promise<Record<number, string>> {
try {
const response = await fetch('/v1/thumbnails/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userIds: ids, size }),
});
if (!response.ok) {
throw new Error(`Failed to fetch user thumbnails: ${response.statusText}`);
}
const data: ThumbnailBatchResponse = await response.json();
// Convert string keys back to numbers
const result: Record<number, string> = {};
Object.entries(data.thumbnails || {}).forEach(([key, value]) => {
result[parseInt(key, 10)] = value;
});
return result;
} catch (error) {
console.error('Error fetching user thumbnails:', error);
return {};
}
}
queueAssetRequest(id: number, size: ThumbnailSize, callback: (url: string | null) => void) {
if (!this.assetQueue.has(id)) {
this.assetQueue.set(id, new Set());
}
this.assetQueue.get(id)!.add(callback);
if (this.assetTimeoutId) {
clearTimeout(this.assetTimeoutId);
}
this.assetTimeoutId = setTimeout(() => {
this.flushAssetQueue(size);
}, this.batchDelay);
}
queueUserRequest(id: number, size: ThumbnailSize, callback: (url: string | null) => void) {
if (!this.userQueue.has(id)) {
this.userQueue.set(id, new Set());
}
this.userQueue.get(id)!.add(callback);
if (this.userTimeoutId) {
clearTimeout(this.userTimeoutId);
}
this.userTimeoutId = setTimeout(() => {
this.flushUserQueue(size);
}, this.batchDelay);
}
private async flushAssetQueue(size: ThumbnailSize) {
if (this.assetQueue.size === 0) return;
const ids = Array.from(this.assetQueue.keys());
const callbacks = new Map(this.assetQueue);
this.assetQueue.clear();
// Split into batches of 100 (API limit)
for (let i = 0; i < ids.length; i += 100) {
const batchIds = ids.slice(i, i + 100);
const results = await this.fetchAssetBatch(batchIds, size);
batchIds.forEach((id) => {
const url = results[id] || null;
const cbs = callbacks.get(id);
if (cbs) {
cbs.forEach((cb) => cb(url));
}
});
}
}
private async flushUserQueue(size: ThumbnailSize) {
if (this.userQueue.size === 0) return;
const ids = Array.from(this.userQueue.keys());
const callbacks = new Map(this.userQueue);
this.userQueue.clear();
// Split into batches of 100 (API limit)
for (let i = 0; i < ids.length; i += 100) {
const batchIds = ids.slice(i, i + 100);
const results = await this.fetchUserBatch(batchIds, size);
batchIds.forEach((id) => {
const url = results[id] || null;
const cbs = callbacks.get(id);
if (cbs) {
cbs.forEach((cb) => cb(url));
}
});
}
}
}
// Singleton batcher instance
const batcher = new ThumbnailBatcher();
/**
* Hook to fetch a single asset thumbnail with automatic batching
*/
export function useAssetThumbnail(assetId: number | undefined, size: ThumbnailSize = '420x420') {
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!assetId) {
setThumbnailUrl(null);
setIsLoading(false);
return;
}
setIsLoading(true);
batcher.queueAssetRequest(assetId, size, (url) => {
setThumbnailUrl(url);
setIsLoading(false);
});
}, [assetId, size]);
return { thumbnailUrl, isLoading };
}
/**
* Hook to fetch a single user avatar thumbnail with automatic batching
*/
export function useUserThumbnail(userId: number | undefined, size: ThumbnailSize = '150x150') {
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!userId) {
setThumbnailUrl(null);
setIsLoading(false);
return;
}
setIsLoading(true);
batcher.queueUserRequest(userId, size, (url) => {
setThumbnailUrl(url);
setIsLoading(false);
});
}, [userId, size]);
return { thumbnailUrl, isLoading };
}
/**
* Hook to prefetch multiple thumbnails (useful for lists)
* This ensures they're batched together
*/
export function usePrefetchThumbnails(
items: Array<{ assetId?: number; userId?: number }>,
assetSize: ThumbnailSize = '420x420',
userSize: ThumbnailSize = '150x150'
) {
useEffect(() => {
items.forEach((item) => {
if (item.assetId) {
batcher.queueAssetRequest(item.assetId, assetSize, () => {});
}
if (item.userId) {
batcher.queueUserRequest(item.userId, userSize, () => {});
}
});
}, [items, assetSize, userSize]);
}

View File

@@ -1,5 +1,3 @@
'use client';
import { useEffect } from 'react'; import { useEffect } from 'react';
export function useTitle(title: string) { export function useTitle(title: string) {

View File

@@ -0,0 +1,39 @@
import { useQuery } from '@tanstack/react-query';
import { UserInfo } from '@/app/ts/User';
async function fetchUser(): Promise<UserInfo | null> {
try {
const response = await fetch('/v1/session/user');
if (!response.ok) {
return null;
}
const userData = await response.json();
if (userData && 'UserID' in userData) {
return userData as UserInfo;
}
return null;
} catch (error) {
console.error('Error fetching user data:', error);
return null;
}
}
export function useUser() {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
staleTime: Infinity, // User data won't go stale unless manually invalidated
gcTime: Infinity, // Keep in cache indefinitely
});
return {
user,
isLoggedIn: user !== null && user !== undefined,
isLoading,
error,
};
}

View File

@@ -0,0 +1,103 @@
import { useEffect, useState } from 'react';
interface UserBatchResponse {
usernames: Record<string, string>;
}
// Batching queue
class UserBatcher {
private queue: Map<number, Set<(name: string | null) => void>> = new Map();
private timeoutId: NodeJS.Timeout | null = null;
private batchDelay = 50; // 50ms delay to collect requests
async fetchBatch(ids: number[]): Promise<Record<number, string>> {
try {
const response = await fetch('/v1/usernames', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userIds: ids }),
});
if (!response.ok) {
throw new Error(`Failed to fetch usernames: ${response.statusText}`);
}
const data: UserBatchResponse = await response.json();
// Convert string keys back to numbers
const result: Record<number, string> = {};
Object.entries(data.usernames || {}).forEach(([key, value]) => {
result[parseInt(key, 10)] = value;
});
return result;
} catch (error) {
console.error('Error fetching usernames:', error);
return {};
}
}
queueRequest(id: number, callback: (name: string | null) => void) {
if (!this.queue.has(id)) {
this.queue.set(id, new Set());
}
this.queue.get(id)!.add(callback);
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.timeoutId = setTimeout(() => {
this.flushQueue();
}, this.batchDelay);
}
private async flushQueue() {
if (this.queue.size === 0) return;
const ids = Array.from(this.queue.keys());
const callbacks = new Map(this.queue);
this.queue.clear();
// Split into batches of 100 (API limit)
for (let i = 0; i < ids.length; i += 100) {
const batchIds = ids.slice(i, i + 100);
const results = await this.fetchBatch(batchIds);
batchIds.forEach((id) => {
const name = results[id] || null;
const cbs = callbacks.get(id);
if (cbs) {
cbs.forEach((cb) => cb(name));
}
});
}
}
}
// Singleton batcher instance
const batcher = new UserBatcher();
/**
* Hook to fetch a single username with automatic batching
*/
export function useUsername(userId: number | undefined) {
const [username, setUsername] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!userId) {
setUsername(null);
setIsLoading(false);
return;
}
setIsLoading(true);
batcher.queueRequest(userId, (name) => {
setUsername(name);
setIsLoading(false);
});
}, [userId]);
return { username, isLoading };
}

View File

@@ -1,17 +0,0 @@
"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 (
<html lang="en">
<body>
<ThemeProvider theme={theme}>
{children}
</ThemeProvider>
</body>
</html>
);
}

View File

@@ -1,35 +0,0 @@
import path from 'path';
import { promises as fs } from 'fs';
import { NextResponse } from 'next/server';
export async function errorImageResponse(
statusCode: number = 500,
options?: { message?: string }
): Promise<NextResponse> {
const file = `${statusCode}.png`;
const filePath = path.join(process.cwd(), 'public/errors', file);
const headers: Record<string, string> = {
'Content-Type': 'image/png',
};
if (options?.message) {
headers['X-Error-Message'] = encodeURIComponent(options.message);
}
try {
const buffer = await fs.readFile(filePath);
headers['Content-Length'] = buffer.length.toString();
return new NextResponse(buffer, {
status: statusCode,
headers,
});
} catch {
const fallback = path.join(process.cwd(), 'public/errors', '500.png');
const buffer = await fs.readFile(fallback);
headers['Content-Length'] = buffer.length.toString();
return new NextResponse(buffer, {
status: 500,
headers,
});
}
}

View File

@@ -1,50 +1,116 @@
import {createTheme} from "@mui/material"; import {createTheme} from "@mui/material";
export const theme = createTheme({ export const theme = createTheme({
cssVariables: {
colorSchemeSelector: 'class',
},
colorSchemes: {
dark: true,
},
defaultColorScheme: 'dark',
palette: { palette: {
mode: 'dark', mode: 'dark',
primary: { primary: {
main: '#90caf9', main: '#3b82f6',
dark: '#2563eb',
light: '#60a5fa',
}, },
secondary: { secondary: {
main: '#f48fb1', main: '#8b5cf6',
dark: '#7c3aed',
light: '#a78bfa',
}, },
background: { background: {
default: '#121212', default: '#0a0a0a',
paper: '#1e1e1e', paper: '#171717',
},
text: {
primary: '#ffffff',
secondary: '#9ca3af',
},
error: {
main: '#ef4444',
light: '#f87171',
dark: '#dc2626',
},
warning: {
main: '#f59e0b',
light: '#fbbf24',
dark: '#d97706',
},
success: {
main: '#10b981',
light: '#34d399',
dark: '#059669',
},
info: {
main: '#3b82f6',
light: '#60a5fa',
dark: '#2563eb',
}, },
}, },
typography: { typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif',
h1: {
fontWeight: 700,
letterSpacing: '-0.025em',
},
h2: {
fontWeight: 700,
letterSpacing: '-0.02em',
},
h3: {
fontWeight: 600,
letterSpacing: '-0.015em',
},
h4: {
fontWeight: 600,
letterSpacing: '-0.01em',
},
h5: { h5: {
fontWeight: 500, fontWeight: 600,
letterSpacing: '0.5px', },
h6: {
fontWeight: 600,
}, },
subtitle1: { subtitle1: {
fontWeight: 500, fontWeight: 500,
fontSize: '0.95rem', fontSize: '1rem',
},
body1: {
fontSize: '1rem',
lineHeight: 1.7,
}, },
body2: { body2: {
fontSize: '0.875rem', fontSize: '0.875rem',
lineHeight: 1.6,
}, },
caption: { caption: {
fontSize: '0.75rem', fontSize: '0.75rem',
}, },
button: {
fontWeight: 600,
textTransform: 'none',
letterSpacing: '0.01em',
},
}, },
shape: { shape: {
borderRadius: 8, borderRadius: 12,
}, },
components: { components: {
MuiCard: { MuiCard: {
styleOverrides: { styleOverrides: {
root: { root: {
borderRadius: 8, borderRadius: 12,
overflow: 'hidden', overflow: 'hidden',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', backgroundColor: '#171717',
transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out', border: '1px solid rgba(255, 255, 255, 0.08)',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': { '&:hover': {
transform: 'translateY(-4px)', transform: 'translateY(-4px)',
boxShadow: '0 8px 16px rgba(0, 0, 0, 0.2)', border: '1px solid rgba(59, 130, 246, 0.4)',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(59, 130, 246, 0.2)',
}, },
}, },
}, },
@@ -52,7 +118,7 @@ export const theme = createTheme({
MuiCardMedia: { MuiCardMedia: {
styleOverrides: { styleOverrides: {
root: { root: {
borderBottom: '1px solid rgba(255, 255, 255, 0.1)', transition: 'transform 0.3s',
}, },
}, },
}, },
@@ -69,14 +135,48 @@ export const theme = createTheme({
MuiChip: { MuiChip: {
styleOverrides: { styleOverrides: {
root: { root: {
fontWeight: 500, fontWeight: 600,
borderRadius: 6,
fontSize: '0.75rem',
transition: 'all 0.2s ease-in-out',
},
icon: {
marginLeft: '8px',
},
colorError: {
backgroundColor: '#ef4444',
color: '#ffffff',
'& .MuiChip-icon': {
color: '#ffffff',
},
},
colorWarning: {
backgroundColor: '#f59e0b',
color: '#ffffff',
'& .MuiChip-icon': {
color: '#ffffff',
},
},
colorSuccess: {
backgroundColor: '#10b981',
color: '#ffffff',
'& .MuiChip-icon': {
color: '#ffffff',
},
},
colorInfo: {
backgroundColor: '#3b82f6',
color: '#ffffff',
'& .MuiChip-icon': {
color: '#ffffff',
},
}, },
}, },
}, },
MuiDivider: { MuiDivider: {
styleOverrides: { styleOverrides: {
root: { root: {
borderColor: 'rgba(255, 255, 255, 0.1)', borderColor: 'rgba(148, 163, 184, 0.1)',
}, },
}, },
}, },
@@ -84,6 +184,126 @@ export const theme = createTheme({
styleOverrides: { styleOverrides: {
root: { root: {
backgroundImage: 'none', backgroundImage: 'none',
backgroundColor: '#171717',
},
},
},
MuiButton: {
styleOverrides: {
root: {
borderRadius: 8,
fontWeight: 600,
textTransform: 'none',
padding: '10px 24px',
transition: 'all 0.2s ease-in-out',
},
contained: {
boxShadow: 'none',
'&:hover': {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
transform: 'translateY(-1px)',
},
},
containedPrimary: {
background: '#3b82f6',
'&:hover': {
background: '#2563eb',
},
},
outlined: {
borderWidth: '1.5px',
'&:hover': {
borderWidth: '1.5px',
backgroundColor: 'rgba(59, 130, 246, 0.08)',
},
},
outlinedPrimary: {
borderColor: 'rgba(59, 130, 246, 0.5)',
'&:hover': {
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.08)',
},
},
outlinedSecondary: {
borderColor: 'rgba(139, 92, 246, 0.5)',
'&:hover': {
borderColor: '#8b5cf6',
backgroundColor: 'rgba(139, 92, 246, 0.08)',
},
},
},
},
MuiAppBar: {
styleOverrides: {
root: {
background: 'rgba(10, 10, 10, 0.8)',
backdropFilter: 'blur(12px)',
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
boxShadow: 'none',
},
},
},
MuiDrawer: {
styleOverrides: {
paper: {
backgroundColor: '#0a0a0a',
borderRight: '1px solid rgba(255, 255, 255, 0.08)',
},
},
},
MuiCircularProgress: {
styleOverrides: {
root: {
color: '#3b82f6',
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
transition: 'all 0.2s ease-in-out',
'&:hover': {
backgroundColor: 'rgba(59, 130, 246, 0.1)',
},
},
},
},
MuiLink: {
styleOverrides: {
root: {
color: '#60a5fa',
textDecoration: 'none',
transition: 'color 0.2s ease-in-out',
'&:hover': {
color: '#3b82f6',
textDecoration: 'underline',
},
},
},
},
MuiMenu: {
styleOverrides: {
paper: {
background: '#171717',
backdropFilter: 'blur(12px)',
border: '1px solid rgba(255, 255, 255, 0.08)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4)',
},
},
},
MuiMenuItem: {
styleOverrides: {
root: {
transition: 'all 0.2s ease-in-out',
'&:hover': {
backgroundColor: 'rgba(59, 130, 246, 0.1)',
},
'&.Mui-selected': {
backgroundColor: 'rgba(59, 130, 246, 0.15)',
'&:hover': {
backgroundColor: 'rgba(59, 130, 246, 0.2)',
},
},
}, },
}, },
}, },

View File

@@ -1,3 +0,0 @@
export const thumbnailLoader = ({ src, width, quality }: { src: string, width: number, quality?: number }) => {
return `${src}?w=${width}&q=${quality || 75}`;
};

View File

@@ -1,9 +1,8 @@
"use client";
import Webpage from "@/app/_components/webpage"; import Webpage from "@/app/_components/webpage";
import { useParams, useRouter } from "next/navigation"; import { useParams, useNavigate } from "react-router-dom";
import {useState} from "react"; import {useState, useEffect} from "react";
import Link from "next/link"; import { Link } from "react-router-dom";
import { useAssetThumbnail } from "@/app/hooks/useThumbnails";
// MUI Components // MUI Components
import { import {
@@ -36,7 +35,7 @@ interface SnackbarState {
export default function MapfixDetailsPage() { export default function MapfixDetailsPage() {
const { mapfixId } = useParams<{ mapfixId: string }>(); const { mapfixId } = useParams<{ mapfixId: string }>();
const router = useRouter(); const navigate = useNavigate();
const [newComment, setNewComment] = useState(""); const [newComment, setNewComment] = useState("");
const [showBeforeImage, setShowBeforeImage] = useState(false); const [showBeforeImage, setShowBeforeImage] = useState(false);
const [snackbar, setSnackbar] = useState<SnackbarState>({ const [snackbar, setSnackbar] = useState<SnackbarState>({
@@ -70,16 +69,26 @@ export default function MapfixDetailsPage() {
refreshData refreshData
} = useReviewData({ } = useReviewData({
itemType: 'mapfixes', itemType: 'mapfixes',
itemId: mapfixId itemId: mapfixId!
}); });
const mapfix = mapfixData as MapfixInfo; const mapfix = mapfixData as MapfixInfo;
useTitle(mapfix ? `${mapfix.DisplayName} Mapfix` : 'Loading Mapfix...'); useTitle(mapfix ? `${mapfix.DisplayName} Mapfix` : 'Loading Mapfix...');
// Use thumbnail hooks for before/after images
const { thumbnailUrl: beforeThumbnail, isLoading: beforeLoading } = useAssetThumbnail(
mapfix?.TargetAssetID,
'420x420'
);
const { thumbnailUrl: afterThumbnail, isLoading: afterLoading } = useAssetThumbnail(
mapfix?.AssetID,
'420x420'
);
// Handle review button actions // Handle review button actions
async function handleReviewAction(action: string, mapfixId: number) { async function handleReviewAction(action: string, mapfixId: number) {
try { try {
const response = await fetch(`/api/mapfixes/${mapfixId}/status/${action}`, { const response = await fetch(`/v1/mapfixes/${mapfixId}/status/${action}`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
@@ -112,13 +121,22 @@ export default function MapfixDetailsPage() {
}; };
// cycle before and after images every 2 seconds
useEffect(() => {
const interval = setInterval(() => {
setShowBeforeImage((prev) => !prev);
}, 2000);
return () => clearInterval(interval);
}, []);
const handleCommentSubmit = async () => { const handleCommentSubmit = async () => {
if (!newComment.trim()) { if (!newComment.trim()) {
return; // Don't submit empty comments return; // Don't submit empty comments
} }
try { try {
const response = await fetch(`/api/mapfixes/${mapfixId}/comment`, { const response = await fetch(`/v1/mapfixes/${mapfixId}/comment`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/plain', 'Content-Type': 'text/plain',
@@ -177,7 +195,7 @@ export default function MapfixDetailsPage() {
title="Error Loading Mapfix" title="Error Loading Mapfix"
message={error || "Mapfix not found"} message={error || "Mapfix not found"}
buttonText="Return to Mapfixes" buttonText="Return to Mapfixes"
onButtonClick={() => router.push('/mapfixes')} onButtonClick={() => navigate('/mapfixes')}
/> />
); );
} }
@@ -191,10 +209,10 @@ export default function MapfixDetailsPage() {
aria-label="breadcrumb" aria-label="breadcrumb"
sx={{ mb: 3 }} sx={{ mb: 3 }}
> >
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Home</Typography> <Typography color="text.primary">Home</Typography>
</Link> </Link>
<Link href="/mapfixes" passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to="/mapfixes" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Mapfixes</Typography> <Typography color="text.primary">Mapfixes</Typography>
</Link> </Link>
<Typography color="text.secondary">{mapfix.DisplayName}</Typography> <Typography color="text.secondary">{mapfix.DisplayName}</Typography>
@@ -207,6 +225,22 @@ export default function MapfixDetailsPage() {
<Box sx={{ position: 'relative', width: '100%', aspectRatio: '1/1' }}> <Box sx={{ position: 'relative', width: '100%', aspectRatio: '1/1' }}>
{/* Before/After Images Container */} {/* Before/After Images Container */}
<Box sx={{ position: 'relative', width: '100%', height: '100%' }}> <Box sx={{ position: 'relative', width: '100%', height: '100%' }}>
{/* Loading Skeleton for Before Image */}
<Skeleton
variant="rectangular"
sx={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 1,
opacity: beforeLoading && showBeforeImage ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
animation="wave"
/>
{/* Before Image */} {/* Before Image */}
<Box <Box
sx={{ sx={{
@@ -216,18 +250,34 @@ export default function MapfixDetailsPage() {
width: '100%', width: '100%',
height: '100%', height: '100%',
zIndex: 1, zIndex: 1,
opacity: showBeforeImage ? 1 : 0, opacity: showBeforeImage ? (beforeLoading ? 0 : 1) : 0,
transition: 'opacity 0.5s ease-in-out' transition: 'opacity 0.5s ease-in-out'
}} }}
> >
<CardMedia <CardMedia
component="img" component="img"
image={`/thumbnails/asset/${mapfix.TargetAssetID}`} image={beforeThumbnail || '/placeholder-map.png'}
alt="Before Map Thumbnail" alt="Before Map Thumbnail"
sx={{ width: '100%', height: '100%', objectFit: 'cover' }} sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
/> />
</Box> </Box>
{/* Loading Skeleton for After Image */}
<Skeleton
variant="rectangular"
sx={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 0,
opacity: afterLoading && !showBeforeImage ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
animation="wave"
/>
{/* After Image */} {/* After Image */}
<Box <Box
sx={{ sx={{
@@ -237,13 +287,13 @@ export default function MapfixDetailsPage() {
width: '100%', width: '100%',
height: '100%', height: '100%',
zIndex: 0, zIndex: 0,
opacity: showBeforeImage ? 0 : 1, opacity: showBeforeImage ? 0 : (afterLoading ? 0 : 1),
transition: 'opacity 0.5s ease-in-out' transition: 'opacity 0.5s ease-in-out'
}} }}
> >
<CardMedia <CardMedia
component="img" component="img"
image={`/thumbnails/asset/${mapfix.AssetID}`} image={afterThumbnail || '/placeholder-map.png'}
alt="After Map Thumbnail" alt="After Map Thumbnail"
sx={{ width: '100%', height: '100%', objectFit: 'cover' }} sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
/> />
@@ -282,33 +332,6 @@ export default function MapfixDetailsPage() {
)} )}
</Box> </Box>
<Box
sx={{
position: 'absolute',
bottom: 16,
left: '50%',
transform: 'translateX(-50%)',
zIndex: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 1,
}}
>
<Typography
variant="caption"
sx={{
color: 'white',
bgcolor: 'rgba(0,0,0,0.4)',
padding: '2px 8px',
borderRadius: 1,
backdropFilter: 'blur(2px)',
}}
>
Click to compare
</Typography>
</Box>
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
@@ -322,7 +345,6 @@ export default function MapfixDetailsPage() {
background: 'linear-gradient(rgba(0,0,0,0.02), rgba(0,0,0,0.05))', background: 'linear-gradient(rgba(0,0,0,0.02), rgba(0,0,0,0.05))',
}, },
}} }}
onClick={() => setShowBeforeImage(!showBeforeImage)}
/> />
</Box> </Box>
</Box> </Box>
@@ -343,6 +365,10 @@ export default function MapfixDetailsPage() {
<ReviewItem <ReviewItem
item={mapfix} item={mapfix}
handleCopyValue={handleCopyId} handleCopyValue={handleCopyId}
currentUserId={user ?? undefined}
userId={user}
onDescriptionUpdate={() => refreshData(true)}
showSnackbar={showSnackbar}
/> />
{/* Comments Section */} {/* Comments Section */}
@@ -353,6 +379,7 @@ export default function MapfixDetailsPage() {
handleCommentSubmit={handleCommentSubmit} handleCommentSubmit={handleCommentSubmit}
validatorUser={validatorUser} validatorUser={validatorUser}
userId={user} userId={user}
currentStatus={mapfix.StatusID}
/> />
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -1,5 +1,3 @@
'use client'
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { MapfixList } from "../ts/Mapfix"; import { MapfixList } from "../ts/Mapfix";
import { MapCard } from "../_components/mapCard"; import { MapCard } from "../_components/mapCard";
@@ -8,12 +6,14 @@ import { ListSortConstants } from "../ts/Sort";
import { import {
Box, Box,
Breadcrumbs, Breadcrumbs,
CircularProgress, Card,
CardContent,
Container, Container,
Pagination, Pagination,
Skeleton,
Typography Typography
} from "@mui/material"; } from "@mui/material";
import Link from "next/link"; import { Link } from "react-router-dom";
import NavigateNextIcon from "@mui/icons-material/NavigateNext"; import NavigateNextIcon from "@mui/icons-material/NavigateNext";
import {useTitle} from "@/app/hooks/useTitle"; import {useTitle} from "@/app/hooks/useTitle";
@@ -32,7 +32,7 @@ export default function MapfixInfoPage() {
setIsLoading(true); setIsLoading(true);
try { try {
const res = await fetch( const res = await fetch(
`/api/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`, `/v1/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`,
{ signal: controller.signal } { signal: controller.signal }
); );
@@ -55,33 +55,38 @@ export default function MapfixInfoPage() {
return () => controller.abort(); return () => controller.abort();
}, [currentPage]); }, [currentPage]);
if (isLoading || !mapfixes) { const skeletonCards = Array.from({ length: cardsPerPage }, (_, i) => i);
const totalPages = mapfixes ? Math.ceil(mapfixes.Total / cardsPerPage) : 0;
if (mapfixes && mapfixes.Total === 0) {
return ( return (
<Webpage> <Webpage>
<Container sx={{ height: '100vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> <Container sx={{ py: 6 }}>
<Box display="flex" flexDirection="column" alignItems="center"> <Typography variant="body1">
<CircularProgress /> Mapfixes list is empty.
<Typography variant="body1" sx={{ mt: 2 }}> </Typography>
Loading mapfixes...
</Typography>
</Box>
</Container> </Container>
</Webpage> </Webpage>
); );
} }
const totalPages = Math.ceil(mapfixes.Total / cardsPerPage);
return ( return (
<Webpage> <Webpage>
<Container maxWidth="lg" sx={{ py: 6 }}> <Box sx={{
<Box component="main" sx={{ width: '100%', px: 2 }}> width: '100%',
display: 'flex',
justifyContent: 'center',
py: 6,
px: 2,
boxSizing: 'border-box'
}}>
<Box sx={{ width: '100%', maxWidth: '1200px', minWidth: 0 }}>
<Breadcrumbs <Breadcrumbs
separator={<NavigateNextIcon fontSize="small" />} separator={<NavigateNextIcon fontSize="small" />}
aria-label="breadcrumb" aria-label="breadcrumb"
sx={{ mb: 3 }} sx={{ mb: 3 }}
> >
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Home</Typography> <Typography color="text.primary">Home</Typography>
</Link> </Link>
<Typography color="text.secondary">Mapfixes</Typography> <Typography color="text.secondary">Mapfixes</Typography>
@@ -99,26 +104,52 @@ export default function MapfixInfoPage() {
className="grid" className="grid"
sx={{ sx={{
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gridTemplateColumns: {
xs: 'repeat(1, 1fr)',
sm: 'repeat(2, 1fr)',
md: 'repeat(3, 1fr)',
lg: 'repeat(4, 1fr)',
},
gap: 3, gap: 3,
width: '100%', width: '100%',
minWidth: 0,
}} }}
> >
{mapfixes.Mapfixes.map((mapfix) => ( {!mapfixes || isLoading ? (
<MapCard skeletonCards.map((i) => (
key={mapfix.ID} <Card key={i} sx={{ height: '100%' }}>
id={mapfix.ID} <Skeleton variant="rectangular" height={180} />
assetId={mapfix.AssetID} <CardContent>
displayName={mapfix.DisplayName} <Skeleton variant="text" width="80%" height={32} sx={{ mb: 1.5 }} />
author={mapfix.Creator} <Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
authorId={mapfix.Submitter} <Skeleton variant="text" width={80} />
rating={mapfix.StatusID} <Skeleton variant="text" width={100} />
statusID={mapfix.StatusID} </Box>
gameID={mapfix.GameID} <Skeleton variant="text" width="60%" />
created={mapfix.CreatedAt} <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 2 }}>
type="mapfix" <Skeleton variant="circular" width={28} height={28} />
/> <Skeleton variant="text" width={100} />
))} </Box>
</CardContent>
</Card>
))
) : (
mapfixes.Mapfixes.map((mapfix) => (
<MapCard
key={mapfix.ID}
id={mapfix.ID}
assetId={mapfix.AssetID}
displayName={mapfix.DisplayName}
author={mapfix.Creator}
authorId={mapfix.Submitter}
rating={mapfix.StatusID}
statusID={mapfix.StatusID}
gameID={mapfix.GameID}
created={mapfix.CreatedAt}
type="mapfix"
/>
))
)}
</Box> </Box>
{totalPages > 1 && ( {totalPages > 1 && (
@@ -133,7 +164,7 @@ export default function MapfixInfoPage() {
</Box> </Box>
)} )}
</Box> </Box>
</Container> </Box>
</Webpage> </Webpage>
); );
} }

View File

@@ -1,5 +1,3 @@
"use client"
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { import {
Button, Button,
@@ -16,10 +14,10 @@ import {
import SendIcon from '@mui/icons-material/Send'; import SendIcon from '@mui/icons-material/Send';
import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import Webpage from "@/app/_components/webpage"; import Webpage from "@/app/_components/webpage";
import { useParams } from "next/navigation"; import { useParams, Link, useNavigate } from "react-router-dom";
import Link from "next/link";
import {MapInfo} from "@/app/ts/Map"; import {MapInfo} from "@/app/ts/Map";
import {useTitle} from "@/app/hooks/useTitle"; import {useTitle} from "@/app/hooks/useTitle";
import { getGameName } from "@/app/utils/games";
interface MapfixPayload { interface MapfixPayload {
AssetID: number; AssetID: number;
@@ -27,15 +25,9 @@ interface MapfixPayload {
Description: string; Description: string;
} }
// Game ID mapping
const gameTypes: Record<number, string> = {
1: "Bhop",
2: "Surf",
5: "Flytrials"
};
export default function MapfixInfoPage() { export default function MapfixInfoPage() {
const { mapId } = useParams<{ mapId: string }>(); const { mapId } = useParams<{ mapId: string }>();
const navigate = useNavigate();
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [mapDetails, setMapDetails] = useState<MapInfo | null>(null); const [mapDetails, setMapDetails] = useState<MapInfo | null>(null);
@@ -48,7 +40,7 @@ export default function MapfixInfoPage() {
const fetchMapDetails = async () => { const fetchMapDetails = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const response = await fetch(`/api/maps/${mapId}`); const response = await fetch(`/v1/maps/${mapId}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch map details: ${response.statusText}`); throw new Error(`Failed to fetch map details: ${response.statusText}`);
@@ -69,12 +61,6 @@ export default function MapfixInfoPage() {
} }
}, [mapId]); }, [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<HTMLFormElement>) => { const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
@@ -106,7 +92,7 @@ export default function MapfixInfoPage() {
}; };
try { try {
const response = await fetch("/api/mapfixes", { const response = await fetch("/v1/mapfixes", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),
@@ -118,7 +104,7 @@ export default function MapfixInfoPage() {
} }
const { OperationID } = await response.json(); const { OperationID } = await response.json();
window.location.assign(`/operations/${OperationID}`); navigate(`/operations/${OperationID}`);
} catch (error) { } catch (error) {
console.error("Error submitting data:", error); console.error("Error submitting data:", error);
setError(error instanceof Error ? error.message : "An unknown error occurred"); setError(error instanceof Error ? error.message : "An unknown error occurred");
@@ -134,14 +120,14 @@ export default function MapfixInfoPage() {
aria-label="breadcrumb" aria-label="breadcrumb"
sx={{ mb: 3 }} sx={{ mb: 3 }}
> >
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Home</Typography> <Typography color="text.primary">Home</Typography>
</Link> </Link>
<Link href="/maps" passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to="/maps" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Maps</Typography> <Typography color="text.primary">Maps</Typography>
</Link> </Link>
{mapDetails && ( {mapDetails && (
<Link href={`/maps/${mapId}`} passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to={`/maps/${mapId}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">{mapDetails.DisplayName}</Typography> <Typography color="text.primary">{mapDetails.DisplayName}</Typography>
</Link> </Link>
)} )}
@@ -201,7 +187,7 @@ export default function MapfixInfoPage() {
label="Game Type" label="Game Type"
variant="outlined" variant="outlined"
fullWidth fullWidth
value={getGameType(mapDetails?.GameID)} value={mapDetails?.GameID ? getGameName(mapDetails.GameID) : "Unknown"}
disabled disabled
/> />
</Grid> </Grid>

View File

@@ -1,13 +1,13 @@
"use client";
import { MapInfo } from "@/app/ts/Map"; import { MapInfo } from "@/app/ts/Map";
import Webpage from "@/app/_components/webpage"; import Webpage from "@/app/_components/webpage";
import { useParams, useRouter } from "next/navigation"; import { useParams, useNavigate } from "react-router-dom";
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Link from "next/link"; import { Link } from "react-router-dom";
import { Snackbar, Alert } from "@mui/material"; import { Snackbar, Alert } from "@mui/material";
import { MapfixStatus, type MapfixInfo } from "@/app/ts/Mapfix"; import { MapfixStatus, type MapfixInfo, getMapfixStatusInfo } from "@/app/ts/Mapfix";
import LaunchIcon from '@mui/icons-material/Launch'; import LaunchIcon from '@mui/icons-material/Launch';
import { useAssetThumbnail } from "@/app/hooks/useThumbnails";
import { getGameInfo } from "@/app/utils/games";
// MUI Components // MUI Components
import { import {
@@ -24,7 +24,11 @@ import {
Stack, Stack,
CardMedia, CardMedia,
Tooltip, Tooltip,
IconButton IconButton,
List,
ListItem,
ListItemIcon,
Pagination
} from "@mui/material"; } from "@mui/material";
import NavigateNextIcon from "@mui/icons-material/NavigateNext"; import NavigateNextIcon from "@mui/icons-material/NavigateNext";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
@@ -34,27 +38,39 @@ import BugReportIcon from "@mui/icons-material/BugReport";
import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
import HistoryIcon from '@mui/icons-material/History';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import CancelIcon from '@mui/icons-material/Cancel';
import BuildIcon from '@mui/icons-material/Build';
import PendingIcon from '@mui/icons-material/Pending';
import {hasRole, RolesConstants} from "@/app/ts/Roles"; import {hasRole, RolesConstants} from "@/app/ts/Roles";
import {useTitle} from "@/app/hooks/useTitle"; import {useTitle} from "@/app/hooks/useTitle";
export default function MapDetails() { export default function MapDetails() {
const { mapId } = useParams(); const { mapId } = useParams();
const router = useRouter(); const navigate = useNavigate();
const [map, setMap] = useState<MapInfo | null>(null); const [map, setMap] = useState<MapInfo | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [copySuccess, setCopySuccess] = useState(false); const [copySuccess, setCopySuccess] = useState(false);
const [roles, setRoles] = useState(RolesConstants.Empty); const [roles, setRoles] = useState(RolesConstants.Empty);
const [mapfixes, setMapfixes] = useState<MapfixInfo[]>([]); const [mapfixes, setMapfixes] = useState<MapfixInfo[]>([]);
const [fixesPage, setFixesPage] = useState(1);
useTitle(map ? `${map.DisplayName}` : 'Loading Map...'); useTitle(map ? `${map.DisplayName}` : 'Loading Map...');
// Use thumbnail hook for the map preview image
const { thumbnailUrl, isLoading: thumbnailLoading } = useAssetThumbnail(
map?.ID,
'768x432'
);
useEffect(() => { useEffect(() => {
async function getMap() { async function getMap() {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
const res = await fetch(`/api/maps/${mapId}`); const res = await fetch(`/v1/maps/${mapId}`);
if (!res.ok) { if (!res.ok) {
throw new Error(`Failed to fetch map: ${res.status}`); throw new Error(`Failed to fetch map: ${res.status}`);
} }
@@ -73,7 +89,7 @@ export default function MapDetails() {
useEffect(() => { useEffect(() => {
async function getRoles() { async function getRoles() {
try { try {
const rolesResponse = await fetch("/api/session/roles"); const rolesResponse = await fetch("/v1/session/roles");
if (rolesResponse.ok) { if (rolesResponse.ok) {
const rolesData = await rolesResponse.json(); const rolesData = await rolesResponse.json();
setRoles(rolesData.Roles); setRoles(rolesData.Roles);
@@ -99,16 +115,15 @@ export default function MapDetails() {
let allMapfixes: MapfixInfo[] = []; let allMapfixes: MapfixInfo[] = [];
let total = 0; let total = 0;
do { do {
const res = await fetch(`/api/mapfixes?Page=${page}&Limit=${limit}&TargetAssetID=${targetAssetId}`); const res = await fetch(`/v1/mapfixes?Page=${page}&Limit=${limit}&TargetAssetID=${targetAssetId}`);
if (!res.ok) break; if (!res.ok) break;
const data = await res.json(); const data = await res.json();
if (page === 1) total = data.Total; if (page === 1) total = data.Total;
allMapfixes = allMapfixes.concat(data.Mapfixes); allMapfixes = allMapfixes.concat(data.Mapfixes);
page++; page++;
} while (allMapfixes.length < total); } while (allMapfixes.length < total);
// Filter out rejected, uploading, uploaded (StatusID > 7) // Store all mapfixes for history display
const active = allMapfixes.filter((fix: MapfixInfo) => fix.StatusID <= MapfixStatus.Validated); setMapfixes(allMapfixes);
setMapfixes(active);
} catch { } catch {
setMapfixes([]); setMapfixes([]);
} }
@@ -124,33 +139,18 @@ export default function MapDetails() {
}); });
}; };
const getGameInfo = (gameId: number) => { const getStatusIcon = (iconName: string) => {
switch (gameId) { switch (iconName) {
case 1: case "Build": return BuildIcon;
return { case "Pending": return PendingIcon;
name: "Bhop", case "CheckCircle": return CheckCircleIcon;
color: "#2196f3" // blue case "Cancel": return CancelIcon;
}; default: return PendingIcon;
case 2:
return {
name: "Surf",
color: "#4caf50" // green
};
case 5:
return {
name: "Fly Trials",
color: "#ff9800" // orange
};
default:
return {
name: "Unknown",
color: "#9e9e9e" // gray
};
} }
}; };
const handleSubmitMapfix = () => { const handleSubmitMapfix = () => {
router.push(`/maps/${mapId}/fix`); navigate(`/maps/${mapId}/fix`);
}; };
const handleCopyId = (idToCopy: string) => { const handleCopyId = (idToCopy: string) => {
@@ -180,7 +180,7 @@ export default function MapDetails() {
<Typography variant="body1">{error}</Typography> <Typography variant="body1">{error}</Typography>
<Button <Button
variant="contained" variant="contained"
onClick={() => router.push('/maps')} onClick={() => navigate('/maps')}
sx={{ mt: 3 }} sx={{ mt: 3 }}
> >
Return to Maps Return to Maps
@@ -200,10 +200,10 @@ export default function MapDetails() {
aria-label="breadcrumb" aria-label="breadcrumb"
sx={{ mb: 3 }} sx={{ mb: 3 }}
> >
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Home</Typography> <Typography color="text.primary">Home</Typography>
</Link> </Link>
<Link href="/maps" passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to="/maps" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Maps</Typography> <Typography color="text.primary">Maps</Typography>
</Link> </Link>
<Typography color="text.secondary">{loading ? "Loading..." : map?.DisplayName || "Map Details"}</Typography> <Typography color="text.secondary">{loading ? "Loading..." : map?.DisplayName || "Map Details"}</Typography>
@@ -299,7 +299,7 @@ export default function MapDetails() {
<IconButton <IconButton
size="small" size="small"
component="a" component="a"
href={`/api/maps/${mapId}/download`} href={`/v1/maps/${mapId}/download`}
download={`${map?.DisplayName}.rbxm`} download={`${map?.DisplayName}.rbxm`}
sx={{ ml: 1 }} sx={{ ml: 1 }}
> >
@@ -319,19 +319,263 @@ export default function MapDetails() {
sx={{ sx={{
borderRadius: 2, borderRadius: 2,
overflow: 'hidden', overflow: 'hidden',
position: 'relative' position: 'relative',
mb: 3
}} }}
> >
<CardMedia <Box sx={{ position: 'relative', overflow: 'hidden' }}>
component="img" <Skeleton
image={`/thumbnails/asset/${map.ID}`} variant="rectangular"
alt={`Preview of map: ${map.DisplayName}`} height={400}
animation="wave"
sx={{ sx={{
height: 400, position: 'absolute',
objectFit: 'cover', top: 0,
left: 0,
width: '100%',
height: '100%',
opacity: thumbnailLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}} }}
/> />
<CardMedia
component="img"
image={thumbnailUrl || '/placeholder-map.png'}
alt={`Preview of map: ${map.DisplayName}`}
sx={{
height: 400,
objectFit: 'cover',
opacity: thumbnailLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}
/>
</Box>
</Paper> </Paper>
{/* Mapfix Section - Active + History */}
{mapfixes.length > 0 && (() => {
const activeFix = mapfixes.find(fix => fix.StatusID !== MapfixStatus.Rejected && fix.StatusID !== MapfixStatus.Released);
const releasedFixes = mapfixes.filter(fix => fix.StatusID === MapfixStatus.Released);
const hasContent = activeFix || releasedFixes.length > 0;
if (!hasContent) return null;
// Pagination for released fixes
const fixesPerPage = 5;
const totalPages = Math.ceil(releasedFixes.length / fixesPerPage);
const startIndex = (fixesPage - 1) * fixesPerPage;
const endIndex = startIndex + fixesPerPage;
const paginatedFixes = releasedFixes
.sort((a, b) => b.CreatedAt - a.CreatedAt)
.slice(startIndex, endIndex);
return (
<Paper elevation={3} sx={{ p: 3, borderRadius: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<HistoryIcon sx={{ mr: 1.5, color: 'primary.main', fontSize: 24 }} />
<Typography variant="h6" component="h2" sx={{ fontWeight: 'bold' }}>
Mapfixes
</Typography>
</Box>
<Divider sx={{ mb: 2 }} />
<List sx={{ width: '100%' }}>
{/* Active Mapfix - shown first with special styling */}
{activeFix && (
<Box key={activeFix.ID}>
<ListItem
component={Link}
to={`/mapfixes/${activeFix.ID}`}
sx={{
py: 2,
px: 2,
borderRadius: 1,
transition: 'all 0.2s',
backgroundColor: 'rgba(25, 118, 210, 0.08)',
borderLeft: '4px solid',
borderColor: 'primary.main',
mb: releasedFixes.length > 0 ? 2 : 0,
'&:hover': {
backgroundColor: 'rgba(25, 118, 210, 0.12)',
transform: 'translateX(4px)'
},
textDecoration: 'none',
color: 'inherit',
display: 'block'
}}
>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<ListItemIcon sx={{ minWidth: 36, mt: 0.5 }}>
{(() => {
const statusInfo = getMapfixStatusInfo(activeFix.StatusID);
const StatusIcon = getStatusIcon(statusInfo.iconName);
return (
<StatusIcon
sx={{
fontSize: 24,
color: statusInfo.color === 'default' ? 'text.secondary' :
statusInfo.color === 'error' ? 'error.main' :
statusInfo.color === 'warning' ? 'warning.main' :
statusInfo.color === 'success' ? 'success.main' :
statusInfo.color === 'primary' ? 'primary.main' : 'info.main'
}}
/>
);
})()}
</ListItemIcon>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography
variant="body1"
component="div"
sx={{
fontWeight: 'bold',
mb: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{activeFix.Description}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 1, flexWrap: 'wrap', alignItems: 'center' }}>
<Chip
label="Active"
size="small"
color="primary"
sx={{ fontWeight: 'bold' }}
/>
<Chip
label={getMapfixStatusInfo(activeFix.StatusID).label}
size="small"
color={getMapfixStatusInfo(activeFix.StatusID).color as any}
sx={{ fontWeight: 'medium' }}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', color: 'text.secondary' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<PersonIcon sx={{ fontSize: 16 }} />
<Typography variant="caption">
{activeFix.Creator}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<CalendarTodayIcon sx={{ fontSize: 16 }} />
<Typography variant="caption">
{formatDate(activeFix.CreatedAt)}
</Typography>
</Box>
</Box>
</Box>
<LaunchIcon sx={{ color: 'primary.main', fontSize: 18, mt: 0.5, flexShrink: 0 }} />
</Box>
</ListItem>
</Box>
)}
{/* Released Fixes History */}
{releasedFixes.length > 0 && (
<>
{activeFix && (
<Box sx={{ mb: 2, mt: 2 }}>
<Divider>
<Chip label={`${releasedFixes.length} Previous Fix${releasedFixes.length !== 1 ? 'es' : ''}`} size="small" />
</Divider>
</Box>
)}
{paginatedFixes.map((fix, index) => {
const statusInfo = getMapfixStatusInfo(fix.StatusID);
const StatusIcon = getStatusIcon(statusInfo.iconName);
return (
<Box key={fix.ID}>
<ListItem
component={Link}
to={`/mapfixes/${fix.ID}`}
sx={{
py: 2,
px: 2,
borderRadius: 1,
transition: 'all 0.2s',
'&:hover': {
backgroundColor: 'action.hover',
transform: 'translateX(4px)'
},
textDecoration: 'none',
color: 'inherit',
display: 'block'
}}
>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<ListItemIcon sx={{ minWidth: 36, mt: 0.5 }}>
<StatusIcon
sx={{
fontSize: 24,
color: 'success.main'
}}
/>
</ListItemIcon>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography
variant="body1"
component="div"
sx={{
fontWeight: 'bold',
mb: 0.5,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{fix.Description}
</Typography>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', color: 'text.secondary' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<PersonIcon sx={{ fontSize: 16 }} />
<Typography variant="caption">
{fix.Creator}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<CalendarTodayIcon sx={{ fontSize: 16 }} />
<Typography variant="caption">
{formatDate(fix.CreatedAt)}
</Typography>
</Box>
</Box>
</Box>
<LaunchIcon sx={{ color: 'primary.main', fontSize: 18, mt: 0.5, flexShrink: 0 }} />
</Box>
</ListItem>
{index < paginatedFixes.length - 1 && <Divider sx={{ my: 1 }} />}
</Box>
);
})}
{/* Pagination */}
{totalPages > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
<Pagination
count={totalPages}
page={fixesPage}
onChange={(_, page) => setFixesPage(page)}
color="primary"
size="medium"
/>
</Box>
)}
</>
)}
</List>
</Paper>
);
})()}
</Grid> </Grid>
{/* Map Details Section */} {/* Map Details Section */}
@@ -376,39 +620,6 @@ export default function MapDetails() {
</Tooltip> </Tooltip>
</Box> </Box>
</Box> </Box>
{/* Active Mapfix in Map Details */}
{mapfixes.length > 0 && (() => {
const active = mapfixes.find(fix => fix.StatusID <= MapfixStatus.Validated);
const latest = mapfixes.reduce((a, b) => (a.CreatedAt > b.CreatedAt ? a : b));
const showFix = active || latest;
return (
<Box>
<Typography variant="subtitle2" color="text.secondary">
Active Mapfix
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography
variant="body2"
component={Link}
href={`/mapfixes/${showFix.ID}`}
sx={{
textDecoration: 'underline',
cursor: 'pointer',
color: 'primary.main',
display: 'flex',
alignItems: 'center',
gap: 0.5,
mt: 0.5
}}
>
{showFix.Description}
<LaunchIcon sx={{ fontSize: '1rem', ml: 0.5 }} />
</Typography>
</Box>
</Box>
);
})()}
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,8 +1,4 @@
"use client";
import {useState, useEffect} from "react"; import {useState, useEffect} from "react";
import Image from "next/image";
import {useRouter} from "next/navigation";
import Webpage from "@/app/_components/webpage"; import Webpage from "@/app/_components/webpage";
import { import {
Box, Box,
@@ -16,18 +12,20 @@ import {
TextField, TextField,
InputAdornment, InputAdornment,
Pagination, Pagination,
CircularProgress,
FormControl, FormControl,
InputLabel, InputLabel,
Select, Select,
MenuItem, MenuItem,
SelectChangeEvent, Breadcrumbs SelectChangeEvent,
Breadcrumbs,
Skeleton
} from "@mui/material"; } from "@mui/material";
import {Search as SearchIcon} from "@mui/icons-material"; import {Search as SearchIcon} from "@mui/icons-material";
import Link from "next/link"; import { Link } from "react-router-dom";
import NavigateNextIcon from "@mui/icons-material/NavigateNext"; import NavigateNextIcon from "@mui/icons-material/NavigateNext";
import {useTitle} from "@/app/hooks/useTitle"; import {useTitle} from "@/app/hooks/useTitle";
import {thumbnailLoader} from '@/app/lib/thumbnailLoader'; import {usePrefetchThumbnails, useAssetThumbnail} from "@/app/hooks/useThumbnails";
import { getGameName, getGameLabelStyles } from "@/app/utils/games";
interface Map { interface Map {
ID: number; ID: number;
@@ -37,10 +35,92 @@ interface Map {
Date: number; Date: number;
} }
interface MapCardProps {
map: Map;
formatDate: (timestamp: number) => string;
}
function MapCard({ map, formatDate }: MapCardProps) {
const { thumbnailUrl, isLoading } = useAssetThumbnail(map.ID, '420x420');
return (
<Card
elevation={1}
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
transition: 'transform 0.2s, box-shadow 0.2s',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: 4,
}
}}
>
<CardActionArea component={Link} to={`/maps/${map.ID}`}>
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
<Skeleton
variant="rectangular"
height={180}
animation="wave"
sx={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
opacity: isLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
/>
<CardMedia
component="img"
image={thumbnailUrl || '/placeholder-map.png'}
alt={map.DisplayName}
sx={{
height: 180,
objectFit: 'cover',
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}
/>
<Box
position="absolute"
top={10}
right={10}
px={1}
py={0.5}
borderRadius={1}
fontSize="0.75rem"
fontWeight="bold"
sx={{
...getGameLabelStyles(map.GameID),
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out 0.1s',
}}
>
{getGameName(map.GameID)}
</Box>
</Box>
<CardContent>
<Typography variant="h6" component="h2" noWrap>
{map.DisplayName}
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
By {map.Creator}
</Typography>
<Typography variant="caption" color="text.secondary">
Added {formatDate(map.Date)}
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
}
export default function MapsPage() { export default function MapsPage() {
useTitle("Map Collection"); useTitle("Map Collection");
const router = useRouter();
const [maps, setMaps] = useState<Map[]>([]); const [maps, setMaps] = useState<Map[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@@ -59,7 +139,7 @@ export default function MapsPage() {
let hasMore = true; let hasMore = true;
while (hasMore) { while (hasMore) {
const res = await fetch(`/api/maps?Page=${page}&Limit=${requestPageSize}`); const res = await fetch(`/v1/maps?Page=${page}&Limit=${requestPageSize}`);
const data: Map[] = await res.json(); const data: Map[] = await res.json();
allMaps = [...allMaps, ...data]; allMaps = [...allMaps, ...data];
hasMore = data.length === requestPageSize; hasMore = data.length === requestPageSize;
@@ -102,15 +182,17 @@ export default function MapsPage() {
currentPage * mapsPerPage currentPage * mapsPerPage
); );
// Prefetch thumbnails for current page maps to batch them together
usePrefetchThumbnails(
currentMaps.map(map => ({ assetId: map.ID })),
'420x420'
);
const handlePageChange = (_event: React.ChangeEvent<unknown>, page: number) => { const handlePageChange = (_event: React.ChangeEvent<unknown>, page: number) => {
setCurrentPage(page); setCurrentPage(page);
window.scrollTo({top: 0, behavior: 'smooth'}); window.scrollTo({top: 0, behavior: 'smooth'});
}; };
const handleMapClick = (mapId: number) => {
router.push(`/maps/${mapId}`);
};
const formatDate = (timestamp: number) => { const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleDateString('en-US', { return new Date(timestamp * 1000).toLocaleDateString('en-US', {
year: 'numeric', year: 'numeric',
@@ -119,44 +201,6 @@ export default function MapsPage() {
}); });
}; };
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 ( return (
<Webpage> <Webpage>
<Container maxWidth="lg" sx={{py: 6}}> <Container maxWidth="lg" sx={{py: 6}}>
@@ -166,7 +210,7 @@ export default function MapsPage() {
aria-label="breadcrumb" aria-label="breadcrumb"
sx={{ mb: 3 }} sx={{ mb: 3 }}
> >
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Home</Typography> <Typography color="text.primary">Home</Typography>
</Link> </Link>
<Typography color="text.secondary">Maps</Typography> <Typography color="text.secondary">Maps</Typography>
@@ -197,9 +241,27 @@ export default function MapsPage() {
/> />
{loading ? ( {loading ? (
<Box display="flex" justifyContent="center" my={8}> <Grid container spacing={3}>
<CircularProgress/> {Array.from({ length: mapsPerPage }).map((_, index) => (
</Box> <Grid size={{ xs: 12, sm: 6, md: 4}} key={index}>
<Card
elevation={1}
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<Skeleton variant="rectangular" height={180} animation="wave" />
<CardContent>
<Skeleton variant="text" width="80%" height={32} sx={{ mb: 1 }} />
<Skeleton variant="text" width="60%" height={20} sx={{ mb: 1 }} />
<Skeleton variant="text" width="40%" height={16} />
</CardContent>
</Card>
</Grid>
))}
</Grid>
) : ( ) : (
<> <>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}> <Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
@@ -227,62 +289,10 @@ export default function MapsPage() {
<Grid container spacing={3}> <Grid container spacing={3}>
{currentMaps.map((map) => ( {currentMaps.map((map) => (
<Grid size={{ xs: 12, sm: 6, md: 4}} key={map.ID}> <Grid size={{ xs: 12, sm: 6, md: 4}} key={map.ID}>
<Card <MapCard
elevation={1} map={map}
sx={{ formatDate={formatDate}
height: '100%', />
display: 'flex',
flexDirection: 'column',
transition: 'transform 0.2s, box-shadow 0.2s',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: 4,
}
}}
>
<CardActionArea onClick={() => handleMapClick(map.ID)}>
<CardMedia
component="div"
sx={{
position: 'relative',
height: 180,
backgroundColor: 'rgba(0,0,0,0.05)',
}}
>
<Box
position="absolute"
top={10}
right={10}
px={1}
py={0.5}
borderRadius={1}
fontSize="0.75rem"
fontWeight="bold"
{...getGameLabelStyles(map.GameID)}
>
{getGameName(map.GameID)}
</Box>
<Image
loader={thumbnailLoader}
src={`/thumbnails/asset/${map.ID}`}
alt={map.DisplayName}
fill
style={{objectFit: 'cover'}}
/>
</CardMedia>
<CardContent>
<Typography variant="h6" component="h2" noWrap>
{map.DisplayName}
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
By {map.Creator}
</Typography>
<Typography variant="caption" color="text.secondary">
Added {formatDate(map.Date)}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Grid> </Grid>
))} ))}
</Grid> </Grid>

View File

@@ -0,0 +1,191 @@
import { Box, Container, Typography, Button } from "@mui/material";
import { Link } from "react-router-dom";
import Webpage from "@/app/_components/webpage";
import { useTitle } from "@/app/hooks/useTitle";
import HomeIcon from "@mui/icons-material/Home";
import MapIcon from "@mui/icons-material/Map";
export default function NotFound() {
useTitle("404 - Page Not Found");
return (
<Webpage>
<Box sx={{ width: '100%', bgcolor: 'background.default' }}>
{/* 404 Hero Section */}
<Box
sx={{
position: 'relative',
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
overflow: 'hidden',
background: 'linear-gradient(to bottom, #0a0a0a 0%, #0f0f0f 100%)',
}}
>
{/* Subtle Gradient Background */}
<Box
sx={{
position: 'absolute',
top: '20%',
right: '30%',
width: '500px',
height: '500px',
background: 'radial-gradient(circle, rgba(239, 68, 68, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
filter: 'blur(80px)',
}}
/>
<Box
sx={{
position: 'absolute',
bottom: '20%',
left: '25%',
width: '450px',
height: '450px',
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.08) 0%, transparent 70%)',
borderRadius: '50%',
filter: 'blur(80px)',
}}
/>
<Container maxWidth="md" sx={{ position: 'relative', zIndex: 1, py: 8 }}>
<Box textAlign="center">
{/* 404 Number */}
<Typography
variant="h1"
sx={{
fontSize: { xs: '6rem', sm: '8rem', md: '10rem' },
fontWeight: 800,
lineHeight: 1,
mb: 2,
letterSpacing: '-0.04em',
background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
}}
>
404
</Typography>
{/* Main Message */}
<Typography
variant="h2"
sx={{
fontWeight: 700,
mb: 2,
letterSpacing: '-0.02em',
fontSize: { xs: '2rem', sm: '2.5rem', md: '3rem' },
}}
>
Page Not Found
</Typography>
{/* Subtext */}
<Typography
variant="h6"
sx={{
color: 'text.secondary',
mb: 6,
maxWidth: '600px',
mx: 'auto',
lineHeight: 1.7,
fontWeight: 400,
fontSize: { xs: '1rem', md: '1.125rem' },
}}
>
Looks like this page doesn't exist. The page you're looking for might have been removed, renamed, or never existed in the first place.
</Typography>
{/* Action Buttons */}
<Box
display="flex"
gap={2.5}
justifyContent="center"
flexWrap="wrap"
mb={8}
>
<Button
component={Link}
to="/"
variant="contained"
size="large"
startIcon={<HomeIcon />}
sx={{
fontSize: '1rem',
px: 4,
py: 1.5,
}}
>
Back to Home
</Button>
<Button
component={Link}
to="/maps"
variant="outlined"
size="large"
startIcon={<MapIcon />}
sx={{
fontSize: '1rem',
px: 4,
py: 1.5,
}}
>
Browse Maps
</Button>
</Box>
{/* Quick Links */}
<Box sx={{ mt: 8 }}>
<Typography
variant="body2"
sx={{
color: 'text.secondary',
mb: 3,
fontSize: '0.875rem',
textTransform: 'uppercase',
letterSpacing: '0.1em',
fontWeight: 600,
}}
>
Quick Links
</Typography>
<Box
sx={{
display: 'flex',
gap: 3,
justifyContent: 'center',
flexWrap: 'wrap',
}}
>
{[
{ label: 'Submissions', path: '/submissions' },
{ label: 'Map Fixes', path: '/mapfixes' },
{ label: 'Submit Map', path: '/submit' },
].map((link) => (
<Button
key={link.path}
component={Link}
to={link.path}
sx={{
color: 'text.secondary',
textTransform: 'none',
fontSize: '1rem',
'&:hover': {
color: 'primary.main',
background: 'rgba(59, 130, 246, 0.1)',
},
}}
>
{link.label}
</Button>
))}
</Box>
</Box>
</Box>
</Container>
</Box>
</Box>
</Webpage>
);
}

View File

@@ -1,7 +1,5 @@
"use client";
import {useEffect, useState, useRef, ReactElement} from "react"; import {useEffect, useState, useRef, ReactElement} from "react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useNavigate } from "react-router-dom";
import { import {
CircularProgress, CircularProgress,
Typography, Typography,
@@ -13,7 +11,11 @@ import {
Divider, Divider,
Alert, Alert,
Collapse, Collapse,
IconButton IconButton,
Fade,
Grow,
Slide,
keyframes
} from "@mui/material"; } from "@mui/material";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import ExpandLessIcon from '@mui/icons-material/ExpandLess';
@@ -23,6 +25,67 @@ import PendingIcon from '@mui/icons-material/Pending';
import Webpage from "@/app/_components/webpage"; import Webpage from "@/app/_components/webpage";
import {useTitle} from "@/app/hooks/useTitle"; import {useTitle} from "@/app/hooks/useTitle";
const pulse = keyframes`
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
`;
const spin = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const slideInUp = keyframes`
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
`;
const successPop = keyframes`
0% {
transform: scale(0.8);
opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;
}
`;
const errorShake = keyframes`
0%, 100% {
transform: translateX(0);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(-5px);
}
20%, 40%, 60%, 80% {
transform: translateX(5px);
}
`;
interface Operation { interface Operation {
OperationID: number; OperationID: number;
Status: number; Status: number;
@@ -33,7 +96,7 @@ interface Operation {
} }
export default function OperationStatusPage() { export default function OperationStatusPage() {
const router = useRouter(); const navigate = useNavigate();
const { operationId } = useParams(); const { operationId } = useParams();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -49,7 +112,7 @@ export default function OperationStatusPage() {
const fetchOperation = async () => { const fetchOperation = async () => {
try { try {
const response = await fetch(`/api/operations/${operationId}`); const response = await fetch(`/v1/operations/${operationId}`);
if (!response.ok) throw new Error("Failed to fetch operation"); if (!response.ok) throw new Error("Failed to fetch operation");
@@ -72,13 +135,12 @@ export default function OperationStatusPage() {
}; };
fetchOperation(); fetchOperation();
if (!intervalRef.current) { intervalRef.current = setInterval(fetchOperation, 1000);
intervalRef.current = setInterval(fetchOperation, 1000);
}
return () => { return () => {
if (intervalRef.current) { if (intervalRef.current) {
clearInterval(intervalRef.current); clearInterval(intervalRef.current);
intervalRef.current = null;
} }
}; };
}, [operationId]); }, [operationId]);
@@ -134,12 +196,27 @@ export default function OperationStatusPage() {
} }
}; };
const getStatusAnimation = (status: number) => {
switch (status) {
case 0:
return pulse;
case 1:
return successPop;
case 2:
return errorShake;
default:
return undefined;
}
};
return ( return (
<Webpage> <Webpage>
<Container maxWidth="md" sx={{ py: 6 }}> <Container maxWidth="md" sx={{ py: 6 }}>
<Typography variant="h4" component="h1" fontWeight="bold" mb={4}> <Fade in timeout={500}>
Operation Status <Typography variant="h4" component="h1" fontWeight="bold" mb={4}>
</Typography> Operation Status
</Typography>
</Fade>
{loading ? ( {loading ? (
<Box display="flex" flexDirection="column" alignItems="center" my={8}> <Box display="flex" flexDirection="column" alignItems="center" my={8}>
@@ -149,33 +226,47 @@ export default function OperationStatusPage() {
</Typography> </Typography>
</Box> </Box>
) : error ? ( ) : error ? (
<Alert severity="error" sx={{ my: 2 }}> <Slide direction="up" in mountOnEnter unmountOnExit>
<Typography variant="body1">{error}</Typography> <Alert severity="error" sx={{ my: 2 }}>
</Alert> <Typography variant="body1">{error}</Typography>
</Alert>
</Slide>
) : operation ? ( ) : operation ? (
<Paper <Grow in timeout={600}>
elevation={3} <Box>
sx={{ <Paper
p: 3, elevation={3}
borderRadius: 2, sx={{
border: 1, p: 3,
borderColor: 'divider' borderRadius: 2,
}} border: 1,
> borderColor: 'divider',
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}> animation: `${slideInUp} 0.5s ease-out`
<Typography variant="h5"> }}
Operation #{operation.OperationID} >
</Typography> <Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
<Chip <Typography variant="h5">
icon={getStatusIcon(operation.Status)} Operation #{operation.OperationID}
label={getStatusText(operation.Status)} </Typography>
color={getStatusColor(operation.Status) as "success" | "warning" | "error" | "default"} <Chip
variant="filled" icon={getStatusIcon(operation.Status)}
sx={{ fontWeight: 'bold', px: 1 }} label={getStatusText(operation.Status)}
/> color={getStatusColor(operation.Status) as "success" | "warning" | "error" | "default"}
</Box> variant="filled"
sx={{
fontWeight: 'bold',
px: 1,
animation: operation.Status === 0
? `${pulse} 2s ease-in-out infinite`
: `${getStatusAnimation(operation.Status)} 0.5s ease-out`,
'& .MuiChip-icon': {
animation: operation.Status === 0 ? `${spin} 2s linear infinite` : 'none'
}
}}
/>
</Box>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography variant="body1" color="text.secondary" gutterBottom> <Typography variant="body1" color="text.secondary" gutterBottom>
@@ -233,24 +324,35 @@ export default function OperationStatusPage() {
)} )}
</Box> </Box>
{operation.Status === 1 && ( {operation.Status === 1 && (
<Box sx={{ mt: 4, textAlign: 'center' }}> <Box sx={{ mt: 4, textAlign: 'center' }}>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
size="large" size="large"
onClick={() => router.push(operation.Path)} onClick={() => navigate(operation.Path)}
startIcon={<CheckCircleIcon />} startIcon={<CheckCircleIcon />}
> sx={{
Next Step animation: `${successPop} 0.6s ease-out`,
</Button> transition: 'transform 0.2s',
</Box> '&:hover': {
)} transform: 'scale(1.05)'
</Paper> }
}}
>
Next Step
</Button>
</Box>
)}
</Paper>
</Box>
</Grow>
) : ( ) : (
<Alert severity="info" sx={{ my: 2 }}> <Fade in>
<Typography variant="body1">No operation found with ID: {operationId}</Typography> <Alert severity="info" sx={{ my: 2 }}>
</Alert> <Typography variant="body1">No operation found with ID: {operationId}</Typography>
</Alert>
</Fade>
)} )}
</Container> </Container>
</Webpage> </Webpage>

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
import { NextResponse } from 'next/server';
export async function GET(
request: Request,
{ params }: { params: Promise<{ userId: string }> }
) {
const { userId } = await params;
if (!userId) {
return NextResponse.json({ error: 'User ID is required' }, { status: 400 });
}
try {
const apiResponse = await fetch(`https://users.roblox.com/v1/users/${userId}`);
if (!apiResponse.ok) {
const errorData = await apiResponse.text();
return NextResponse.json({ error: `Failed to fetch from Roblox API: ${errorData}` }, { status: apiResponse.status });
}
const data = await apiResponse.json();
// Add caching headers to the response
const headers = new Headers();
headers.set('Cache-Control', 'public, max-age=3600, s-maxage=3600'); // Cache for 1 hour
return NextResponse.json(data, { headers });
} catch {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}

View File

@@ -0,0 +1,830 @@
import { useState, useEffect } from "react";
import { SubmissionList, SubmissionStatus } from "../ts/Submission";
import { MapfixList, MapfixStatus } from "../ts/Mapfix";
import { MapCard } from "../_components/mapCard";
import Webpage from "@/app/_components/webpage";
import { ListSortConstants } from "../ts/Sort";
import { RolesConstants, hasRole, hasAnyReviewerRole } from "../ts/Roles";
import { useUser } from "@/app/hooks/useUser";
import {
Box,
Breadcrumbs,
Card,
CardContent,
Container,
Skeleton,
Typography,
Tabs,
Tab,
Alert,
CircularProgress,
Chip,
Button
} from "@mui/material";
import { Link } from "react-router-dom";
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
import { useTitle } from "@/app/hooks/useTitle";
import AssignmentIcon from "@mui/icons-material/Assignment";
import BuildIcon from "@mui/icons-material/Build";
import CodeIcon from "@mui/icons-material/Code";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`reviewer-tabpanel-${index}`}
aria-labelledby={`reviewer-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ pt: 3 }}>{children}</Box>}
</div>
);
}
// Helper function to get submission statuses based on user roles
function getSubmissionStatusesForRoles(roles: number): SubmissionStatus[] {
const statuses: SubmissionStatus[] = [];
if (hasRole(roles, RolesConstants.SubmissionUpload)) {
statuses.push(SubmissionStatus.Validated);
}
if (hasRole(roles, RolesConstants.SubmissionReview)) {
statuses.push(SubmissionStatus.Submitted);
}
if (hasRole(roles, RolesConstants.SubmissionRelease)) {
statuses.push(SubmissionStatus.Uploaded);
}
if (hasRole(roles, RolesConstants.ScriptWrite)) {
statuses.push(SubmissionStatus.AcceptedUnvalidated);
}
return statuses;
}
// Helper function to get mapfix statuses based on user roles
function getMapfixStatusesForRoles(roles: number): MapfixStatus[] {
const statuses: MapfixStatus[] = [];
if (hasRole(roles, RolesConstants.ScriptWrite)) {
statuses.push(MapfixStatus.AcceptedUnvalidated);
}
if (hasRole(roles, RolesConstants.MapfixUpload)) {
statuses.push(MapfixStatus.Validated);
statuses.push(MapfixStatus.Uploaded);
}
if (hasRole(roles, RolesConstants.MapfixReview)) {
statuses.push(MapfixStatus.Submitted);
}
return statuses;
}
// Group submissions by status with priority ordering
// Priority order: ScriptWrite > SubmissionRelease > SubmissionUpload > SubmissionReview
function groupSubmissionsByStatus(submissions: any[], roles: number) {
const groups: { status: SubmissionStatus; label: string; items: any[]; priority: number }[] = [];
// Add groups in priority order based on user's roles
if (hasRole(roles, RolesConstants.ScriptWrite)) {
const items = submissions.filter(s => s.StatusID === SubmissionStatus.AcceptedUnvalidated);
if (items.length > 0) {
groups.push({ status: SubmissionStatus.AcceptedUnvalidated, label: 'Script Review', items, priority: 1 });
}
}
if (hasRole(roles, RolesConstants.SubmissionRelease)) {
const items = submissions.filter(s => s.StatusID === SubmissionStatus.Uploaded);
if (items.length > 0) {
groups.push({ status: SubmissionStatus.Uploaded, label: 'Ready to Release', items, priority: 2 });
}
}
if (hasRole(roles, RolesConstants.SubmissionUpload)) {
const items = submissions.filter(s => s.StatusID === SubmissionStatus.Validated);
if (items.length > 0) {
groups.push({ status: SubmissionStatus.Validated, label: 'Ready to Upload', items, priority: 3 });
}
}
if (hasRole(roles, RolesConstants.SubmissionReview)) {
const items = submissions.filter(s => s.StatusID === SubmissionStatus.Submitted);
if (items.length > 0) {
groups.push({ status: SubmissionStatus.Submitted, label: 'Pending Review', items, priority: 4 });
}
}
return groups;
}
// Group mapfixes by status with priority ordering
// Priority order: ScriptWrite > MapfixUpload > MapfixReview
function groupMapfixesByStatus(mapfixes: any[], roles: number) {
const groups: { status: MapfixStatus; label: string; items: any[]; priority: number }[] = [];
// Add groups in priority order based on user's roles
if (hasRole(roles, RolesConstants.ScriptWrite)) {
const items = mapfixes.filter(m => m.StatusID === MapfixStatus.AcceptedUnvalidated);
if (items.length > 0) {
groups.push({ status: MapfixStatus.AcceptedUnvalidated, label: 'Script Review', items, priority: 1 });
}
}
if (hasRole(roles, RolesConstants.MapfixUpload)) {
const validated = mapfixes.filter(m => m.StatusID === MapfixStatus.Validated);
const uploaded = mapfixes.filter(m => m.StatusID === MapfixStatus.Uploaded);
if (validated.length > 0) {
groups.push({ status: MapfixStatus.Validated, label: 'Ready to Upload', items: validated, priority: 2 });
}
if (uploaded.length > 0) {
groups.push({ status: MapfixStatus.Uploaded, label: 'Ready to Release', items: uploaded, priority: 3 });
}
}
if (hasRole(roles, RolesConstants.MapfixReview)) {
const items = mapfixes.filter(m => m.StatusID === MapfixStatus.Submitted);
if (items.length > 0) {
groups.push({ status: MapfixStatus.Submitted, label: 'Pending Review', items, priority: 4 });
}
}
return groups;
}
export default function ReviewerDashboardPage() {
useTitle("Reviewer Dashboard");
const { user, isLoading: userLoading } = useUser();
const [submissions, setSubmissions] = useState<SubmissionList | null>(null);
const [mapfixes, setMapfixes] = useState<MapfixList | null>(null);
const [isLoadingSubmissions, setIsLoadingSubmissions] = useState(false);
const [isLoadingMapfixes, setIsLoadingMapfixes] = useState(false);
const [tabValue, setTabValue] = useState(0);
const [userRoles, setUserRoles] = useState<number | null>(null);
const [scriptPoliciesCount, setScriptPoliciesCount] = useState<number>(0);
const [isLoadingScripts, setIsLoadingScripts] = useState(false);
// Fetch user roles
useEffect(() => {
// Fetch roles from API
const controller = new AbortController();
async function fetchRoles() {
try {
const res = await fetch('/v1/session/roles', { signal: controller.signal });
if (res.ok) {
const data = await res.json();
setUserRoles(data.Roles);
}
} catch (error) {
if (!(error instanceof DOMException && error.name === 'AbortError')) {
console.error("Error fetching roles:", error);
}
}
}
if (user) {
fetchRoles();
}
return () => controller.abort();
}, [user]);
// Fetch submissions needing review
useEffect(() => {
const controller = new AbortController();
async function fetchAllPagesForStatus(status: SubmissionStatus): Promise<any[]> {
const allItems: any[] = [];
let page = 1;
let hasMore = true;
let totalCount = 0;
while (hasMore) {
const res = await fetch(
`/v1/submissions?Page=${page}&Limit=100&Sort=${ListSortConstants.ListSortDateAscending}&StatusID=${status}`,
{ signal: controller.signal }
);
if (!res.ok) {
console.error(`Failed to fetch submissions for status ${status}, page ${page}:`, res.status);
break;
}
const data = await res.json();
// Store the total count from the first response
if (page === 1) {
totalCount = data.Total || 0;
}
if (data.Submissions && data.Submissions.length > 0) {
allItems.push(...data.Submissions);
// Check if there are more pages based on the total count
hasMore = allItems.length < totalCount;
page++;
} else {
hasMore = false;
}
}
return allItems;
}
async function fetchSubmissions() {
if (!userRoles) return;
setIsLoadingSubmissions(true);
try {
// Get statuses based on user roles and deduplicate
const allowedStatuses = getSubmissionStatusesForRoles(userRoles);
const uniqueStatuses = Array.from(new Set(allowedStatuses));
// Fetch all pages for each status in parallel
const results = await Promise.all(
uniqueStatuses.map(status => fetchAllPagesForStatus(status))
);
// Combine all results
const allSubmissions = results.flat();
setSubmissions({
Submissions: allSubmissions,
Total: allSubmissions.length
});
} catch (error) {
if (!(error instanceof DOMException && error.name === 'AbortError')) {
console.error("Error fetching submissions:", error);
}
} finally {
setIsLoadingSubmissions(false);
}
}
if (userRoles && (
hasRole(userRoles, RolesConstants.SubmissionReview) ||
hasRole(userRoles, RolesConstants.SubmissionUpload) ||
hasRole(userRoles, RolesConstants.SubmissionRelease) ||
hasRole(userRoles, RolesConstants.ScriptWrite)
)) {
fetchSubmissions();
}
return () => controller.abort();
}, [userRoles]);
// Fetch mapfixes needing review
useEffect(() => {
const controller = new AbortController();
async function fetchAllPagesForStatus(status: MapfixStatus): Promise<any[]> {
const allItems: any[] = [];
let page = 1;
let hasMore = true;
let totalCount = 0;
while (hasMore) {
const res = await fetch(
`/v1/mapfixes?Page=${page}&Limit=100&Sort=${ListSortConstants.ListSortDateAscending}&StatusID=${status}`,
{ signal: controller.signal }
);
if (!res.ok) {
console.error(`Failed to fetch mapfixes for status ${status}, page ${page}:`, res.status);
break;
}
const data = await res.json();
// Store the total count from the first response
if (page === 1) {
totalCount = data.Total || 0;
}
if (data.Mapfixes && data.Mapfixes.length > 0) {
allItems.push(...data.Mapfixes);
// Check if there are more pages based on the total count
hasMore = allItems.length < totalCount;
page++;
} else {
hasMore = false;
}
}
return allItems;
}
async function fetchMapfixes() {
if (!userRoles) return;
setIsLoadingMapfixes(true);
try {
// Get statuses based on user roles and deduplicate
const allowedStatuses = getMapfixStatusesForRoles(userRoles);
const uniqueStatuses = Array.from(new Set(allowedStatuses));
// Fetch all pages for each status in parallel
const results = await Promise.all(
uniqueStatuses.map(status => fetchAllPagesForStatus(status))
);
// Combine all results
const allMapfixes = results.flat();
setMapfixes({
Mapfixes: allMapfixes,
Total: allMapfixes.length
});
} catch (error) {
if (!(error instanceof DOMException && error.name === 'AbortError')) {
console.error("Error fetching mapfixes:", error);
}
} finally {
setIsLoadingMapfixes(false);
}
}
if (userRoles && (
hasRole(userRoles, RolesConstants.MapfixReview) ||
hasRole(userRoles, RolesConstants.MapfixUpload) ||
hasRole(userRoles, RolesConstants.ScriptWrite)
)) {
fetchMapfixes();
}
return () => controller.abort();
}, [userRoles]);
// Fetch script policies needing review
useEffect(() => {
const controller = new AbortController();
async function fetchScriptPolicies() {
if (!userRoles || !hasRole(userRoles, RolesConstants.ScriptWrite)) return;
setIsLoadingScripts(true);
try {
const res = await fetch(
'/v1/script-policy?Page=1&Limit=50&Policy=0',
{ signal: controller.signal }
);
if (!res.ok) {
console.error('Failed to fetch script policies:', res.status);
setIsLoadingScripts(false);
return;
}
const policies = await res.json();
// The API does not provide total count, so we use the length of returned policies
setScriptPoliciesCount(Array.isArray(policies) ? policies.length : 0);
} catch (error) {
if (!(error instanceof DOMException && error.name === 'AbortError')) {
console.error("Error fetching script policies:", error);
}
} finally {
setIsLoadingScripts(false);
}
}
if (userRoles) {
fetchScriptPolicies();
}
return () => controller.abort();
}, [userRoles]);
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
const skeletonCards = Array.from({ length: 12 }, (_, i) => i);
// Check if user is loading
if (userLoading) {
return (
<Webpage>
<Container sx={{ py: 6, display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Container>
</Webpage>
);
}
// Check if user is logged in
if (!user) {
return (
<Webpage>
<Container sx={{ py: 6 }}>
<Alert severity="warning">
You must be logged in to access the reviewer dashboard.
</Alert>
</Container>
</Webpage>
);
}
// Wait for roles to load before checking permissions
if (userRoles === null) {
return (
<Webpage>
<Container sx={{ py: 6, display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Container>
</Webpage>
);
}
// Check if user has any reviewer permissions
const canReviewSubmissions = (
hasRole(userRoles, RolesConstants.SubmissionReview) ||
hasRole(userRoles, RolesConstants.SubmissionUpload) ||
hasRole(userRoles, RolesConstants.SubmissionRelease) ||
hasRole(userRoles, RolesConstants.ScriptWrite)
);
const canReviewMapfixes = (
hasRole(userRoles, RolesConstants.MapfixReview) ||
hasRole(userRoles, RolesConstants.MapfixUpload) ||
hasRole(userRoles, RolesConstants.ScriptWrite)
);
const canReviewScripts = hasRole(userRoles, RolesConstants.ScriptWrite);
if (!hasAnyReviewerRole(userRoles)) {
return (
<Webpage>
<Container sx={{ py: 6 }}>
<Alert severity="error">
You do not have permission to access the reviewer dashboard. This page is only available to users with review permissions.
</Alert>
</Container>
</Webpage>
);
}
return (
<Webpage>
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
py: 6,
px: 2,
boxSizing: 'border-box'
}}>
<Box sx={{ width: '100%', maxWidth: '1200px', minWidth: 0 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Breadcrumbs
separator={<NavigateNextIcon fontSize="small" />}
aria-label="breadcrumb"
>
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Home</Typography>
</Link>
<Typography color="text.secondary">Reviewer Dashboard</Typography>
</Breadcrumbs>
<Button
component={Link}
to="/dashboard"
variant="outlined"
size="small"
startIcon={<AssignmentIcon />}
>
User Dashboard
</Button>
</Box>
<Typography variant="h3" component="h1" fontWeight="bold" mb={2}>
Reviewer Dashboard
</Typography>
<Typography variant="subtitle1" color="text.secondary" mb={4}>
Manage submissions and map fixes requiring your attention.
</Typography>
{/* Summary Cards */}
<Box sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr', md: canReviewScripts ? '1fr 1fr 1fr' : '1fr 1fr' },
gap: 3,
mb: 4
}}>
{canReviewSubmissions && (
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<AssignmentIcon sx={{ fontSize: 40, color: 'primary.main' }} />
<Box>
<Typography variant="h4" fontWeight="bold">
{isLoadingSubmissions ? (
<Skeleton width={50} />
) : (
userRoles && submissions
? groupSubmissionsByStatus(submissions.Submissions, userRoles).reduce((sum, group) => sum + group.items.length, 0)
: 0
)}
</Typography>
<Typography variant="body2" color="text.secondary">
Submissions Pending Review
</Typography>
</Box>
</Box>
</CardContent>
</Card>
)}
{canReviewMapfixes && (
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<BuildIcon sx={{ fontSize: 40, color: 'secondary.main' }} />
<Box>
<Typography variant="h4" fontWeight="bold">
{isLoadingMapfixes ? (
<Skeleton width={50} />
) : (
userRoles && mapfixes
? groupMapfixesByStatus(mapfixes.Mapfixes, userRoles).reduce((sum, group) => sum + group.items.length, 0)
: 0
)}
</Typography>
<Typography variant="body2" color="text.secondary">
Map Fixes Pending Review
</Typography>
</Box>
</Box>
</CardContent>
</Card>
)}
{canReviewScripts && (
<Card
component={Link}
to="/script-review"
sx={{
textDecoration: 'none',
cursor: 'pointer',
transition: 'transform 0.2s, box-shadow 0.2s',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: 4
}
}}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<CodeIcon sx={{ fontSize: 40, color: 'success.main' }} />
<Box>
<Typography variant="h4" fontWeight="bold">
{isLoadingScripts ? (
<Skeleton width={50} />
) : (
scriptPoliciesCount >= 50 ? '50+' : scriptPoliciesCount
)}
</Typography>
<Typography variant="body2" color="text.secondary">
Scripts Pending Review
</Typography>
</Box>
</Box>
<ArrowForwardIcon sx={{ fontSize: 24, color: 'text.secondary' }} />
</Box>
</CardContent>
</Card>
)}
</Box>
{/* Tabs */}
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="reviewer tabs">
{canReviewSubmissions && (
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
Submissions
{!isLoadingSubmissions && userRoles && submissions && (() => {
const total = groupSubmissionsByStatus(submissions.Submissions, userRoles).reduce((sum, group) => sum + group.items.length, 0);
return total > 0 ? (
<Chip
label={total}
size="small"
color="primary"
/>
) : null;
})()}
</Box>
}
id="reviewer-tab-0"
aria-controls="reviewer-tabpanel-0"
/>
)}
{canReviewMapfixes && (
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
Map Fixes
{!isLoadingMapfixes && userRoles && mapfixes && (() => {
const total = groupMapfixesByStatus(mapfixes.Mapfixes, userRoles).reduce((sum, group) => sum + group.items.length, 0);
return total > 0 ? (
<Chip
label={total}
size="small"
color="secondary"
/>
) : null;
})()}
</Box>
}
id={`reviewer-tab-${canReviewSubmissions ? 1 : 0}`}
aria-controls={`reviewer-tabpanel-${canReviewSubmissions ? 1 : 0}`}
/>
)}
</Tabs>
</Box>
{/* Submissions Tab */}
{canReviewSubmissions && (
<TabPanel value={tabValue} index={0}>
{userRoles && submissions && groupSubmissionsByStatus(submissions.Submissions, userRoles).reduce((sum, group) => sum + group.items.length, 0) === 0 ? (
<Alert severity="success">
No submissions currently need your review. Great job!
</Alert>
) : isLoadingSubmissions ? (
<Box
className="grid"
sx={{
display: 'grid',
gridTemplateColumns: {
xs: 'repeat(1, 1fr)',
sm: 'repeat(2, 1fr)',
md: 'repeat(3, 1fr)',
lg: 'repeat(4, 1fr)',
},
gap: 3,
width: '100%',
minWidth: 0,
}}
>
{skeletonCards.map((i) => (
<Card key={i} sx={{ height: '100%' }}>
<Skeleton variant="rectangular" height={180} />
<CardContent>
<Skeleton variant="text" width="80%" height={32} sx={{ mb: 1.5 }} />
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
<Skeleton variant="text" width={80} />
<Skeleton variant="text" width={100} />
</Box>
<Skeleton variant="text" width="60%" />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 2 }}>
<Skeleton variant="circular" width={28} height={28} />
<Skeleton variant="text" width={100} />
</Box>
</CardContent>
</Card>
))}
</Box>
) : (
<Box>
{userRoles && submissions && groupSubmissionsByStatus(submissions.Submissions, userRoles).map((group, groupIdx) => (
<Box key={groupIdx} sx={{ mb: groupIdx < groupSubmissionsByStatus(submissions.Submissions, userRoles).length - 1 ? 4 : 0 }}>
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2 }}>
{group.label} ({group.items.length})
</Typography>
<Box
className="grid"
sx={{
display: 'grid',
gridTemplateColumns: {
xs: 'repeat(1, 1fr)',
sm: 'repeat(2, 1fr)',
md: 'repeat(3, 1fr)',
lg: 'repeat(4, 1fr)',
},
gap: 3,
width: '100%',
minWidth: 0,
}}
>
{group.items.map((submission) => (
<MapCard
key={submission.ID}
id={submission.ID}
assetId={submission.AssetID}
displayName={submission.DisplayName}
author={submission.Creator}
authorId={submission.Submitter}
rating={submission.StatusID}
statusID={submission.StatusID}
gameID={submission.GameID}
created={submission.CreatedAt}
type="submission"
/>
))}
</Box>
</Box>
))}
</Box>
)}
</TabPanel>
)}
{/* Map Fixes Tab */}
{canReviewMapfixes && (
<TabPanel value={tabValue} index={canReviewSubmissions ? 1 : 0}>
{userRoles && mapfixes && groupMapfixesByStatus(mapfixes.Mapfixes, userRoles).reduce((sum, group) => sum + group.items.length, 0) === 0 ? (
<Alert severity="success">
No map fixes currently need your review. Great job!
</Alert>
) : isLoadingMapfixes ? (
<Box
className="grid"
sx={{
display: 'grid',
gridTemplateColumns: {
xs: 'repeat(1, 1fr)',
sm: 'repeat(2, 1fr)',
md: 'repeat(3, 1fr)',
lg: 'repeat(4, 1fr)',
},
gap: 3,
width: '100%',
minWidth: 0,
}}
>
{skeletonCards.map((i) => (
<Card key={i} sx={{ height: '100%' }}>
<Skeleton variant="rectangular" height={180} />
<CardContent>
<Skeleton variant="text" width="80%" height={32} sx={{ mb: 1.5 }} />
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
<Skeleton variant="text" width={80} />
<Skeleton variant="text" width={100} />
</Box>
<Skeleton variant="text" width="60%" />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 2 }}>
<Skeleton variant="circular" width={28} height={28} />
<Skeleton variant="text" width={100} />
</Box>
</CardContent>
</Card>
))}
</Box>
) : (
<Box>
{userRoles && mapfixes && groupMapfixesByStatus(mapfixes.Mapfixes, userRoles).map((group, groupIdx) => (
<Box key={groupIdx} sx={{ mb: groupIdx < groupMapfixesByStatus(mapfixes.Mapfixes, userRoles).length - 1 ? 4 : 0 }}>
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2 }}>
{group.label} ({group.items.length})
</Typography>
<Box
className="grid"
sx={{
display: 'grid',
gridTemplateColumns: {
xs: 'repeat(1, 1fr)',
sm: 'repeat(2, 1fr)',
md: 'repeat(3, 1fr)',
lg: 'repeat(4, 1fr)',
},
gap: 3,
width: '100%',
minWidth: 0,
}}
>
{group.items.map((mapfix) => (
<MapCard
key={mapfix.ID}
id={mapfix.ID}
assetId={mapfix.AssetID}
displayName={mapfix.DisplayName}
author={mapfix.Creator}
authorId={mapfix.Submitter}
rating={mapfix.StatusID}
statusID={mapfix.StatusID}
gameID={mapfix.GameID}
created={mapfix.CreatedAt}
type="mapfix"
/>
))}
</Box>
</Box>
))}
</Box>
)}
</TabPanel>
)}
</Box>
</Box>
</Webpage>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
"use client";
import Webpage from "@/app/_components/webpage"; import Webpage from "@/app/_components/webpage";
import { useParams, useRouter } from "next/navigation"; import { useParams, useNavigate } from "react-router-dom";
import {useState} from "react"; import {useState} from "react";
import Link from "next/link"; import { Link } from "react-router-dom";
import { useAssetThumbnail } from "@/app/hooks/useThumbnails";
// MUI Components // MUI Components
import { import {
@@ -35,7 +35,7 @@ interface SnackbarState {
export default function SubmissionDetailsPage() { export default function SubmissionDetailsPage() {
const { submissionId } = useParams<{ submissionId: string }>(); const { submissionId } = useParams<{ submissionId: string }>();
const router = useRouter(); const navigate = useNavigate();
const [newComment, setNewComment] = useState(""); const [newComment, setNewComment] = useState("");
const [snackbar, setSnackbar] = useState<SnackbarState>({ const [snackbar, setSnackbar] = useState<SnackbarState>({
open: false, open: false,
@@ -70,16 +70,22 @@ export default function SubmissionDetailsPage() {
refreshData refreshData
} = useReviewData({ } = useReviewData({
itemType: 'submissions', itemType: 'submissions',
itemId: submissionId itemId: submissionId!
}); });
const submission = submissionData as SubmissionInfo; const submission = submissionData as SubmissionInfo;
useTitle(submission ? `${submission.DisplayName} Submission` : 'Loading Submission...'); useTitle(submission ? `${submission.DisplayName} Submission` : 'Loading Submission...');
// Use thumbnail hook for the submission image
const { thumbnailUrl, isLoading: thumbnailLoading } = useAssetThumbnail(
submission?.AssetID,
'420x420'
);
// Handle review button actions // Handle review button actions
async function handleReviewAction(action: string, submissionId: number) { async function handleReviewAction(action: string, submissionId: number) {
try { try {
const response = await fetch(`/api/submissions/${submissionId}/status/${action}`, { const response = await fetch(`/v1/submissions/${submissionId}/status/${action}`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
@@ -118,7 +124,7 @@ export default function SubmissionDetailsPage() {
} }
try { try {
const response = await fetch(`/api/submissions/${submissionId}/comment`, { const response = await fetch(`/v1/submissions/${submissionId}/comment`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/plain', 'Content-Type': 'text/plain',
@@ -177,7 +183,7 @@ export default function SubmissionDetailsPage() {
title="Error Loading Submission" title="Error Loading Submission"
message={error || "Submission not found"} message={error || "Submission not found"}
buttonText="Return to Submissions" buttonText="Return to Submissions"
onButtonClick={() => router.push('/submissions')} onButtonClick={() => navigate('/submissions')}
/> />
); );
} }
@@ -190,10 +196,10 @@ export default function SubmissionDetailsPage() {
aria-label="breadcrumb" aria-label="breadcrumb"
sx={{ mb: 3 }} sx={{ mb: 3 }}
> >
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Home</Typography> <Typography color="text.primary">Home</Typography>
</Link> </Link>
<Link href="/submissions" passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to="/submissions" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Submissions</Typography> <Typography color="text.primary">Submissions</Typography>
</Link> </Link>
<Typography color="text.secondary">{submission.DisplayName}</Typography> <Typography color="text.secondary">{submission.DisplayName}</Typography>
@@ -204,12 +210,33 @@ export default function SubmissionDetailsPage() {
<Grid size={{ xs: 12, md: 4}}> <Grid size={{ xs: 12, md: 4}}>
<Paper elevation={3} sx={{ borderRadius: 2, overflow: 'hidden', mb: 3 }}> <Paper elevation={3} sx={{ borderRadius: 2, overflow: 'hidden', mb: 3 }}>
{submission.AssetID ? ( {submission.AssetID ? (
<CardMedia <Box sx={{ position: 'relative', overflow: 'hidden' }}>
component="img" <Skeleton
image={`/thumbnails/asset/${submission.AssetID}`} variant="rectangular"
alt="Map Thumbnail" sx={{
sx={{ width: '100%', aspectRatio: '1/1', objectFit: 'cover' }} position: 'absolute',
/> top: 0,
left: 0,
width: '100%',
aspectRatio: '1/1',
opacity: thumbnailLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
animation="wave"
/>
<CardMedia
component="img"
image={thumbnailUrl || '/placeholder-map.png'}
alt="Map Thumbnail"
sx={{
width: '100%',
aspectRatio: '1/1',
objectFit: 'cover',
opacity: thumbnailLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}
/>
</Box>
) : ( ) : (
<Box <Box
sx={{ sx={{
@@ -240,6 +267,7 @@ export default function SubmissionDetailsPage() {
<ReviewItem <ReviewItem
item={submission} item={submission}
handleCopyValue={handleCopyId} handleCopyValue={handleCopyId}
currentUserId={user ?? undefined}
/> />
{/* Comments Section */} {/* Comments Section */}
@@ -250,6 +278,7 @@ export default function SubmissionDetailsPage() {
handleCommentSubmit={handleCommentSubmit} handleCommentSubmit={handleCommentSubmit}
validatorUser={validatorUser} validatorUser={validatorUser}
userId={user} userId={user}
currentStatus={submission.StatusID}
/> />
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -1,5 +1,3 @@
"use client"
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { SubmissionList } from "../ts/Submission"; import { SubmissionList } from "../ts/Submission";
import { MapCard } from "../_components/mapCard"; import { MapCard } from "../_components/mapCard";
@@ -8,12 +6,14 @@ import { ListSortConstants } from "../ts/Sort";
import { import {
Box, Box,
Breadcrumbs, Breadcrumbs,
CircularProgress, Card,
CardContent,
Container, Container,
Pagination, Pagination,
Skeleton,
Typography Typography
} from "@mui/material"; } from "@mui/material";
import Link from "next/link"; import { Link } from "react-router-dom";
import NavigateNextIcon from "@mui/icons-material/NavigateNext"; import NavigateNextIcon from "@mui/icons-material/NavigateNext";
import {useTitle} from "@/app/hooks/useTitle"; import {useTitle} from "@/app/hooks/useTitle";
@@ -32,7 +32,7 @@ export default function SubmissionInfoPage() {
setIsLoading(true); setIsLoading(true);
try { try {
const res = await fetch( const res = await fetch(
`/api/submissions?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`, `/v1/submissions?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`,
{ signal: controller.signal } { signal: controller.signal }
); );
@@ -55,24 +55,10 @@ export default function SubmissionInfoPage() {
return () => controller.abort(); return () => controller.abort();
}, [currentPage]); }, [currentPage]);
if (isLoading || !submissions) { const skeletonCards = Array.from({ length: cardsPerPage }, (_, i) => i);
return ( const totalPages = submissions ? Math.ceil(submissions.Total / cardsPerPage) : 0;
<Webpage>
<Container sx={{ height: '100vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Box display="flex" flexDirection="column" alignItems="center">
<CircularProgress />
<Typography variant="body1" sx={{ mt: 2 }}>
Loading submissions...
</Typography>
</Box>
</Container>
</Webpage>
);
}
const totalPages = Math.ceil(submissions.Total / cardsPerPage); if (submissions && submissions.Total === 0) {
if (submissions.Total === 0) {
return ( return (
<Webpage> <Webpage>
<Container sx={{ py: 6 }}> <Container sx={{ py: 6 }}>
@@ -86,14 +72,21 @@ export default function SubmissionInfoPage() {
return ( return (
<Webpage> <Webpage>
<Container maxWidth="lg" sx={{ py: 6 }}> <Box sx={{
<Box component="main" sx={{ width: '100%', px: 2 }}> width: '100%',
display: 'flex',
justifyContent: 'center',
py: 6,
px: 2,
boxSizing: 'border-box'
}}>
<Box sx={{ width: '100%', maxWidth: '1200px', minWidth: 0 }}>
<Breadcrumbs <Breadcrumbs
separator={<NavigateNextIcon fontSize="small" />} separator={<NavigateNextIcon fontSize="small" />}
aria-label="breadcrumb" aria-label="breadcrumb"
sx={{ mb: 3 }} sx={{ mb: 3 }}
> >
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}> <Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
<Typography color="text.primary">Home</Typography> <Typography color="text.primary">Home</Typography>
</Link> </Link>
<Typography color="text.secondary">Submissions</Typography> <Typography color="text.secondary">Submissions</Typography>
@@ -111,26 +104,52 @@ export default function SubmissionInfoPage() {
className="grid" className="grid"
sx={{ sx={{
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gridTemplateColumns: {
xs: 'repeat(1, 1fr)',
sm: 'repeat(2, 1fr)',
md: 'repeat(3, 1fr)',
lg: 'repeat(4, 1fr)',
},
gap: 3, gap: 3,
width: '100%', width: '100%',
minWidth: 0,
}} }}
> >
{submissions.Submissions.map((submission) => ( {!submissions || isLoading ? (
<MapCard skeletonCards.map((i) => (
key={submission.ID} <Card key={i} sx={{ height: '100%' }}>
id={submission.ID} <Skeleton variant="rectangular" height={180} />
assetId={submission.AssetID} <CardContent>
displayName={submission.DisplayName} <Skeleton variant="text" width="80%" height={32} sx={{ mb: 1.5 }} />
author={submission.Creator} <Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
authorId={submission.Submitter} <Skeleton variant="text" width={80} />
rating={submission.StatusID} <Skeleton variant="text" width={100} />
statusID={submission.StatusID} </Box>
gameID={submission.GameID} <Skeleton variant="text" width="60%" />
created={submission.CreatedAt} <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 2 }}>
type="submission" <Skeleton variant="circular" width={28} height={28} />
/> <Skeleton variant="text" width={100} />
))} </Box>
</CardContent>
</Card>
))
) : (
submissions.Submissions.map((submission) => (
<MapCard
key={submission.ID}
id={submission.ID}
assetId={submission.AssetID}
displayName={submission.DisplayName}
author={submission.Creator}
authorId={submission.Submitter}
rating={submission.StatusID}
statusID={submission.StatusID}
gameID={submission.GameID}
created={submission.CreatedAt}
type="submission"
/>
))
)}
</Box> </Box>
{totalPages > 1 && ( {totalPages > 1 && (
@@ -145,7 +164,7 @@ export default function SubmissionInfoPage() {
</Box> </Box>
)} )}
</Box> </Box>
</Container> </Box>
</Webpage> </Webpage>
); );
} }

Some files were not shown because too many files have changed in this diff Show More