Compare commits
77 Commits
rm-alloc
...
mapfix-che
| Author | SHA1 | Date | |
|---|---|---|---|
|
474655f4a3
|
|||
|
9e47ca5177
|
|||
|
8894231b41
|
|||
|
19a6b0304c
|
|||
|
f00b2b8473
|
|||
|
7fdd72ffdd
|
|||
| 264ce38c08 | |||
|
b1e10dc50e
|
|||
|
755616f46c
|
|||
| e41d34dd3d | |||
| f49e27e230 | |||
| d500462fc7 | |||
| ee2bc94312 | |||
| 84edc71574 | |||
| 7c5d8a2163 | |||
| 7eaa84a0ed | |||
| cf0cf9da7a | |||
| 74565e567a | |||
| ea65794255 | |||
| 58706a5687 | |||
| efeb525e19 | |||
|
5a1fe60a7b
|
|||
| 01cfe67848 | |||
| a19bc4d380 | |||
| ae006565d6 | |||
| 57bca99109 | |||
| cd09c9b18e | |||
| e48cbaff72 | |||
| 140d58b808 | |||
| ba761549b8 | |||
| 86643fef8d | |||
| 96af864c5e | |||
| 7db89fd99b | |||
| f2bb1b078d | |||
| 66878fba4e | |||
| bda99550be | |||
| 8a216c7e82 | |||
| e5277c05a1 | |||
| e4af76cfd4 | |||
| 30db1cc375 | |||
| b50c84f8cf | |||
| 7589ef7df6 | |||
| 8ab8c441b0 | |||
| a26b228ebe | |||
| 3654755540 | |||
| c2b50ffab2 | |||
| 75756917b1 | |||
| 8989c08857 | |||
| b2232f4177 | |||
| 7d1c4d2b6c | |||
| ca401d4b96 | |||
|
9ab80931bf
|
|||
|
09022e7292
|
|||
| 47c0fff0ec | |||
| b7c28616ad | |||
| 89ab25dfb9 | |||
| b0b5ff0725 | |||
| 0532965d37 | |||
| f59979987f | |||
| a232269d54 | |||
| a7c4ca4b49 | |||
| ca9f82a5aa | |||
| e1a2f6f075 | |||
| dad904cd86 | |||
| ad7117a69c | |||
| d566591ea6 | |||
| 424ef6238b | |||
| 0f0ab4d3e0 | |||
| 3e2d782289 | |||
| dc446c545f | |||
|
e234a87d05
|
|||
| 8ab772ea81 | |||
| 9b58b1d26a | |||
| 7689001e74 | |||
| e89abed3d5 | |||
| b792d33164 | |||
| 929b5949f0 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1044,6 +1044,7 @@ dependencies = [
|
||||
"rust-grpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"siphasher",
|
||||
"tokio",
|
||||
"tonic",
|
||||
|
||||
@@ -34,7 +34,7 @@ services:
|
||||
"--data-rpc-host","dataservice:9000",
|
||||
]
|
||||
env_file:
|
||||
- ~/auth-compose/strafesnet_staging.env
|
||||
- /home/quat/auth-compose/strafesnet_staging.env
|
||||
depends_on:
|
||||
- authrpc
|
||||
- nats
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
maptest-validator
|
||||
container_name: validation
|
||||
env_file:
|
||||
- ~/auth-compose/strafesnet_staging.env
|
||||
- /home/quat/auth-compose/strafesnet_staging.env
|
||||
environment:
|
||||
- ROBLOX_GROUP_ID=17032139 # "None" is special case string value
|
||||
- API_HOST_INTERNAL=http://submissions:8083/v1
|
||||
@@ -105,7 +105,7 @@ services:
|
||||
- REDIS_ADDR=authredis:6379
|
||||
- RBX_GROUP_ID=17032139
|
||||
env_file:
|
||||
- ~/auth-compose/auth-service.env
|
||||
- /home/quat/auth-compose/auth-service.env
|
||||
depends_on:
|
||||
- authredis
|
||||
networks:
|
||||
@@ -119,7 +119,7 @@ services:
|
||||
environment:
|
||||
- REDIS_ADDR=authredis:6379
|
||||
env_file:
|
||||
- ~/auth-compose/auth-service.env
|
||||
- /home/quat/auth-compose/auth-service.env
|
||||
depends_on:
|
||||
- authredis
|
||||
networks:
|
||||
|
||||
45
go.mod
45
go.mod
@@ -11,17 +11,18 @@ require (
|
||||
github.com/dchest/siphash v1.2.3
|
||||
github.com/gin-gonic/gin v1.10.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/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/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/urfave/cli/v2 v2.27.6
|
||||
go.opentelemetry.io/otel v1.32.0
|
||||
go.opentelemetry.io/otel/metric v1.32.0
|
||||
go.opentelemetry.io/otel/trace v1.32.0
|
||||
go.opentelemetry.io/otel v1.39.0
|
||||
go.opentelemetry.io/otel/metric v1.39.0
|
||||
go.opentelemetry.io/otel/trace v1.39.0
|
||||
google.golang.org/grpc v1.48.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.25.12
|
||||
@@ -33,9 +34,11 @@ require (
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // 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/iasm v0.2.0 // 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/gin-contrib/sse v0.1.0 // 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/josharian/intern v1.0.0 // 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/leodido/go-urn v1.4.0 // 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/pelletier/go-toml/v2 v2.2.2 // 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/ugorji/go/codec v1.2.12 // 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/crypto v0.32.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // 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/golang/protobuf v1.5.4 // 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/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/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
69
go.sum
69
go.sum
@@ -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/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
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/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
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/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.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/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
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/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
|
||||
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/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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
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/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
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/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
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/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.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/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.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
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/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
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.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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
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/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=
|
||||
@@ -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/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.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.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
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/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.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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
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/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.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/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
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/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/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
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/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.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.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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.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/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
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/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
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/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
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/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.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.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
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.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
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-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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
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.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.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-20180826012351-8a410e7b638d/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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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.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.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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
307
openapi.yaml
307
openapi.yaml
@@ -14,15 +14,41 @@ tags:
|
||||
description: Long-running operations
|
||||
- name: Session
|
||||
description: Session queries
|
||||
- name: Stats
|
||||
description: Statistics queries
|
||||
- name: Submissions
|
||||
description: Submission operations
|
||||
- name: Scripts
|
||||
description: Script operations
|
||||
- name: ScriptPolicy
|
||||
description: Script policy operations
|
||||
- name: Thumbnails
|
||||
description: Thumbnail operations
|
||||
- name: Users
|
||||
description: User operations
|
||||
security:
|
||||
- cookieAuth: []
|
||||
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:
|
||||
get:
|
||||
summary: Get information about the currently logged in user
|
||||
@@ -421,6 +447,30 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$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:
|
||||
post:
|
||||
summary: Called by maptest when a player completes the map
|
||||
@@ -1438,6 +1488,222 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$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:
|
||||
securitySchemes:
|
||||
cookieAuth:
|
||||
@@ -2061,6 +2327,47 @@ components:
|
||||
type: integer
|
||||
format: int32
|
||||
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:
|
||||
description: Represents error object
|
||||
type: object
|
||||
|
||||
@@ -5,14 +5,14 @@ package api
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
ht "github.com/ogen-go/ogen/http"
|
||||
"github.com/ogen-go/ogen/middleware"
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
"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 (
|
||||
@@ -32,6 +32,7 @@ type otelConfig struct {
|
||||
Tracer trace.Tracer
|
||||
MeterProvider metric.MeterProvider
|
||||
Meter metric.Meter
|
||||
Attributes []attribute.KeyValue
|
||||
}
|
||||
|
||||
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.
|
||||
func WithClient(client ht.Client) ClientOption {
|
||||
return optionFunc[clientConfig](func(cfg *clientConfig) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
19
pkg/api/oas_defaults_gen.go
Normal file
19
pkg/api/oas_defaults_gen.go
Normal 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
@@ -30,6 +30,9 @@ const (
|
||||
ActionSubmissionTriggerUploadOperation OperationName = "ActionSubmissionTriggerUpload"
|
||||
ActionSubmissionTriggerValidateOperation OperationName = "ActionSubmissionTriggerValidate"
|
||||
ActionSubmissionValidatedOperation OperationName = "ActionSubmissionValidated"
|
||||
BatchAssetThumbnailsOperation OperationName = "BatchAssetThumbnails"
|
||||
BatchUserThumbnailsOperation OperationName = "BatchUserThumbnails"
|
||||
BatchUsernamesOperation OperationName = "BatchUsernames"
|
||||
CreateMapfixOperation OperationName = "CreateMapfix"
|
||||
CreateMapfixAuditCommentOperation OperationName = "CreateMapfixAuditComment"
|
||||
CreateScriptOperation OperationName = "CreateScript"
|
||||
@@ -40,12 +43,15 @@ const (
|
||||
DeleteScriptOperation OperationName = "DeleteScript"
|
||||
DeleteScriptPolicyOperation OperationName = "DeleteScriptPolicy"
|
||||
DownloadMapAssetOperation OperationName = "DownloadMapAsset"
|
||||
GetAssetThumbnailOperation OperationName = "GetAssetThumbnail"
|
||||
GetMapOperation OperationName = "GetMap"
|
||||
GetMapfixOperation OperationName = "GetMapfix"
|
||||
GetOperationOperation OperationName = "GetOperation"
|
||||
GetScriptOperation OperationName = "GetScript"
|
||||
GetScriptPolicyOperation OperationName = "GetScriptPolicy"
|
||||
GetStatsOperation OperationName = "GetStats"
|
||||
GetSubmissionOperation OperationName = "GetSubmission"
|
||||
GetUserThumbnailOperation OperationName = "GetUserThumbnail"
|
||||
ListMapfixAuditEventsOperation OperationName = "ListMapfixAuditEvents"
|
||||
ListMapfixesOperation OperationName = "ListMapfixes"
|
||||
ListMapsOperation OperationName = "ListMaps"
|
||||
@@ -59,6 +65,7 @@ const (
|
||||
SessionValidateOperation OperationName = "SessionValidate"
|
||||
SetMapfixCompletedOperation OperationName = "SetMapfixCompleted"
|
||||
SetSubmissionCompletedOperation OperationName = "SetSubmissionCompleted"
|
||||
UpdateMapfixDescriptionOperation OperationName = "UpdateMapfixDescription"
|
||||
UpdateMapfixModelOperation OperationName = "UpdateMapfixModel"
|
||||
UpdateScriptOperation OperationName = "UpdateScript"
|
||||
UpdateScriptPolicyOperation OperationName = "UpdateScriptPolicy"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
@@ -10,13 +11,13 @@ import (
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/go-faster/jx"
|
||||
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
"github.com/ogen-go/ogen/validate"
|
||||
)
|
||||
|
||||
func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||
req *MapfixTriggerCreate,
|
||||
func (s *Server) decodeBatchAssetThumbnailsRequest(r *http.Request) (
|
||||
req *BatchAssetThumbnailsReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -37,22 +38,266 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
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 == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
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 {
|
||||
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)
|
||||
|
||||
var request MapfixTriggerCreate
|
||||
@@ -70,7 +315,7 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -78,16 +323,17 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||
}
|
||||
return 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:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
|
||||
req CreateMapfixAuditCommentReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -108,20 +354,21 @@ func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
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 := CreateMapfixAuditCommentReq{Data: reader}
|
||||
return request, close, nil
|
||||
return request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
||||
req *ScriptCreate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -142,22 +389,29 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
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 == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
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 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request ScriptCreate
|
||||
@@ -175,7 +429,7 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -183,16 +437,17 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
||||
}
|
||||
return 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:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
||||
req *ScriptPolicyCreate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -213,22 +468,29 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
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 == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
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 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request ScriptPolicyCreate
|
||||
@@ -246,7 +508,7 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -254,16 +516,17 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
||||
}
|
||||
return 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:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
||||
req *SubmissionTriggerCreate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -284,22 +547,29 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
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 == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
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 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request SubmissionTriggerCreate
|
||||
@@ -317,7 +587,7 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -325,16 +595,17 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
||||
}
|
||||
return 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:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
||||
req *SubmissionTriggerCreate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -355,22 +626,29 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
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 == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
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 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request SubmissionTriggerCreate
|
||||
@@ -388,7 +666,7 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -396,16 +674,17 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
||||
}
|
||||
return 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:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
|
||||
req CreateSubmissionAuditCommentReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -426,20 +705,21 @@ func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
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 := CreateSubmissionAuditCommentReq{Data: reader}
|
||||
return request, close, nil
|
||||
return request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||
req []ReleaseInfo,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -460,22 +740,29 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
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 == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
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 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request []ReleaseInfo
|
||||
@@ -501,7 +788,7 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if request == nil {
|
||||
@@ -534,16 +821,17 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||
}
|
||||
return 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:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
||||
req *ScriptUpdate,
|
||||
func (s *Server) decodeUpdateMapfixDescriptionRequest(r *http.Request) (
|
||||
req UpdateMapfixDescriptionReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -564,22 +852,64 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
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 {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
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 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request ScriptUpdate
|
||||
@@ -597,7 +927,7 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -605,16 +935,17 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
||||
}
|
||||
return 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:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
||||
req *ScriptPolicyUpdate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -635,22 +966,29 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
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 == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
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 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request ScriptPolicyUpdate
|
||||
@@ -668,7 +1006,7 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -676,10 +1014,10 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
||||
}
|
||||
return 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:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,51 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-faster/jx"
|
||||
|
||||
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(
|
||||
req *MapfixTriggerCreate,
|
||||
r *http.Request,
|
||||
@@ -119,6 +160,16 @@ func encodeReleaseSubmissionsRequest(
|
||||
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(
|
||||
req *ScriptUpdate,
|
||||
r *http.Request,
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/go-faster/jx"
|
||||
|
||||
"github.com/ogen-go/ogen/conv"
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
"github.com/ogen-go/ogen/uri"
|
||||
"github.com/ogen-go/ogen/validate"
|
||||
)
|
||||
|
||||
@@ -1456,6 +1457,282 @@ func decodeActionSubmissionValidatedResponse(resp *http.Response) (res *ActionSu
|
||||
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) {
|
||||
switch resp.StatusCode {
|
||||
case 201:
|
||||
@@ -2277,6 +2554,105 @@ func decodeDownloadMapAssetResponse(resp *http.Response) (res DownloadMapAssetOK
|
||||
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) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
@@ -2782,6 +3158,107 @@ func decodeGetScriptPolicyResponse(resp *http.Response) (res *ScriptPolicy, _ er
|
||||
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) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
@@ -2883,6 +3360,105 @@ func decodeGetSubmissionResponse(resp *http.Response) (res *Submission, _ 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) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
@@ -4232,6 +4808,66 @@ func decodeSetSubmissionCompletedResponse(resp *http.Response) (res *SetSubmissi
|
||||
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) {
|
||||
switch resp.StatusCode {
|
||||
case 204:
|
||||
|
||||
@@ -8,10 +8,11 @@ import (
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"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/trace"
|
||||
|
||||
ht "github.com/ogen-go/ogen/http"
|
||||
)
|
||||
|
||||
func encodeActionMapfixAcceptedResponse(response *ActionMapfixAcceptedNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||
@@ -182,6 +183,48 @@ func encodeActionSubmissionValidatedResponse(response *ActionSubmissionValidated
|
||||
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 {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(201)
|
||||
@@ -296,6 +339,32 @@ func encodeDownloadMapAssetResponse(response DownloadMapAssetOK, w http.Response
|
||||
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 {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -366,6 +435,20 @@ func encodeGetScriptPolicyResponse(response *ScriptPolicy, w http.ResponseWriter
|
||||
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 {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -380,6 +463,32 @@ func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, sp
|
||||
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 {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -568,6 +677,13 @@ func encodeSetSubmissionCompletedResponse(response *SetSubmissionCompletedNoCont
|
||||
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 {
|
||||
w.WriteHeader(204)
|
||||
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||
|
||||
@@ -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"
|
||||
|
||||
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"
|
||||
|
||||
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.
|
||||
type Route struct {
|
||||
name string
|
||||
summary string
|
||||
operationID string
|
||||
pathPattern string
|
||||
count int
|
||||
args [1]string
|
||||
name string
|
||||
summary string
|
||||
operationID string
|
||||
operationGroup string
|
||||
pathPattern string
|
||||
count int
|
||||
args [1]string
|
||||
}
|
||||
|
||||
// Name returns ogen operation name.
|
||||
@@ -1465,6 +1672,11 @@ func (r Route) OperationID() string {
|
||||
return r.operationID
|
||||
}
|
||||
|
||||
// OperationGroup returns the x-ogen-operation-group value.
|
||||
func (r Route) OperationGroup() string {
|
||||
return r.operationGroup
|
||||
}
|
||||
|
||||
// PathPattern returns OpenAPI path.
|
||||
func (r Route) PathPattern() string {
|
||||
return r.pathPattern
|
||||
@@ -1551,6 +1763,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ListMapfixesOperation
|
||||
r.summary = "Get list of mapfixes"
|
||||
r.operationID = "listMapfixes"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -1559,6 +1772,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = CreateMapfixOperation
|
||||
r.summary = "Trigger the validator to create a mapfix"
|
||||
r.operationID = "createMapfix"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -1591,6 +1805,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = GetMapfixOperation
|
||||
r.summary = "Retrieve map with ID"
|
||||
r.operationID = "getMapfix"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1627,6 +1842,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ListMapfixAuditEventsOperation
|
||||
r.summary = "Retrieve a list of audit events"
|
||||
r.operationID = "listMapfixAuditEvents"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/audit-events"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1663,6 +1879,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = CreateMapfixAuditCommentOperation
|
||||
r.summary = "Post a comment to the audit log"
|
||||
r.operationID = "createMapfixAuditComment"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/comment"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1687,6 +1904,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = SetMapfixCompletedOperation
|
||||
r.summary = "Called by maptest when a player completes the map"
|
||||
r.operationID = "setMapfixCompleted"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/completed"
|
||||
r.args = args
|
||||
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"
|
||||
|
||||
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.summary = "Update model following role restrictions"
|
||||
r.operationID = "updateMapfixModel"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/model"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1761,6 +2005,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixRejectOperation
|
||||
r.summary = "Role Reviewer changes status from Submitted -> Rejected"
|
||||
r.operationID = "actionMapfixReject"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/reject"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1785,6 +2030,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixRequestChangesOperation
|
||||
r.summary = "Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested"
|
||||
r.operationID = "actionMapfixRequestChanges"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/request-changes"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1821,6 +2067,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixUploadedOperation
|
||||
r.summary = "Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded"
|
||||
r.operationID = "actionMapfixUploaded"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-releasing"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1845,6 +2092,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixResetSubmittingOperation
|
||||
r.summary = "Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction"
|
||||
r.operationID = "actionMapfixResetSubmitting"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-submitting"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1869,6 +2117,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixValidatedOperation
|
||||
r.summary = "Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated"
|
||||
r.operationID = "actionMapfixValidated"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-uploading"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1893,6 +2142,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixAcceptedOperation
|
||||
r.summary = "Role Reviewer manually resets validating softlock and changes status from Validating -> Accepted"
|
||||
r.operationID = "actionMapfixAccepted"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-validating"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1919,6 +2169,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixRetryValidateOperation
|
||||
r.summary = "Role Reviewer re-runs validation and changes status from Accepted -> Validating"
|
||||
r.operationID = "actionMapfixRetryValidate"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/retry-validate"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1943,6 +2194,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixRevokeOperation
|
||||
r.summary = "Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction"
|
||||
r.operationID = "actionMapfixRevoke"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/revoke"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -1981,6 +2233,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixTriggerReleaseOperation
|
||||
r.summary = "Role MapfixUpload changes status from Uploaded -> Releasing"
|
||||
r.operationID = "actionMapfixTriggerRelease"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-release"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2004,6 +2257,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixTriggerSubmitOperation
|
||||
r.summary = "Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting"
|
||||
r.operationID = "actionMapfixTriggerSubmit"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-submit"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2028,6 +2282,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixTriggerSubmitUncheckedOperation
|
||||
r.summary = "Role Reviewer changes status from ChangesRequested -> Submitting"
|
||||
r.operationID = "actionMapfixTriggerSubmitUnchecked"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-submit-unchecked"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2054,6 +2309,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixTriggerUploadOperation
|
||||
r.summary = "Role MapfixUpload changes status from Validated -> Uploading"
|
||||
r.operationID = "actionMapfixTriggerUpload"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-upload"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2078,6 +2334,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionMapfixTriggerValidateOperation
|
||||
r.summary = "Role Reviewer triggers validation and changes status from Submitted -> Validating"
|
||||
r.operationID = "actionMapfixTriggerValidate"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-validate"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2111,6 +2368,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ListMapsOperation
|
||||
r.summary = "Get list of maps"
|
||||
r.operationID = "listMaps"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/maps"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -2143,6 +2401,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = GetMapOperation
|
||||
r.summary = "Retrieve map with ID"
|
||||
r.operationID = "getMap"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/maps/{MapID}"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2167,6 +2426,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = DownloadMapAssetOperation
|
||||
r.summary = "Download the map asset"
|
||||
r.operationID = "downloadMapAsset"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/maps/{MapID}/download"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2206,6 +2466,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = GetOperationOperation
|
||||
r.summary = "Retrieve operation with ID"
|
||||
r.operationID = "getOperation"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/operations/{OperationID}"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2230,6 +2491,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ReleaseSubmissionsOperation
|
||||
r.summary = "Release a set of uploaded maps. Role SubmissionRelease"
|
||||
r.operationID = "releaseSubmissions"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/release-submissions"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -2277,6 +2539,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ListScriptPolicyOperation
|
||||
r.summary = "Get list of script policies"
|
||||
r.operationID = "listScriptPolicy"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/script-policy"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -2285,6 +2548,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = CreateScriptPolicyOperation
|
||||
r.summary = "Create a new script policy"
|
||||
r.operationID = "createScriptPolicy"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/script-policy"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -2318,6 +2582,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = DeleteScriptPolicyOperation
|
||||
r.summary = "Delete the specified script policy by ID"
|
||||
r.operationID = "deleteScriptPolicy"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/script-policy/{ScriptPolicyID}"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2326,6 +2591,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = GetScriptPolicyOperation
|
||||
r.summary = "Get the specified script policy by ID"
|
||||
r.operationID = "getScriptPolicy"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/script-policy/{ScriptPolicyID}"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2334,6 +2600,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = UpdateScriptPolicyOperation
|
||||
r.summary = "Update the specified script policy by ID"
|
||||
r.operationID = "updateScriptPolicy"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/script-policy/{ScriptPolicyID}"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2359,6 +2626,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ListScriptsOperation
|
||||
r.summary = "Get list of scripts"
|
||||
r.operationID = "listScripts"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/scripts"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -2367,6 +2635,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = CreateScriptOperation
|
||||
r.summary = "Create a new script"
|
||||
r.operationID = "createScript"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/scripts"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -2400,6 +2669,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = DeleteScriptOperation
|
||||
r.summary = "Delete the specified script by ID"
|
||||
r.operationID = "deleteScript"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/scripts/{ScriptID}"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2408,6 +2678,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = GetScriptOperation
|
||||
r.summary = "Get the specified script by ID"
|
||||
r.operationID = "getScript"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/scripts/{ScriptID}"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2416,6 +2687,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = UpdateScriptOperation
|
||||
r.summary = "Update the specified script by ID"
|
||||
r.operationID = "updateScript"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/scripts/{ScriptID}"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2456,6 +2728,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = SessionRolesOperation
|
||||
r.summary = "Get list of roles for the current session"
|
||||
r.operationID = "sessionRoles"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/session/roles"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -2480,6 +2753,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = SessionUserOperation
|
||||
r.summary = "Get information about the currently logged in user"
|
||||
r.operationID = "sessionUser"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/session/user"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -2504,6 +2778,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = SessionValidateOperation
|
||||
r.summary = "Ask if the current session is valid"
|
||||
r.operationID = "sessionValidate"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/session/validate"
|
||||
r.args = args
|
||||
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"
|
||||
|
||||
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.summary = "Get list of submissions"
|
||||
r.operationID = "listSubmissions"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -2537,6 +2838,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = CreateSubmissionOperation
|
||||
r.summary = "Trigger the validator to create a new submission"
|
||||
r.operationID = "createSubmission"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -2561,6 +2863,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = CreateSubmissionAdminOperation
|
||||
r.summary = "Trigger the validator to create a new submission"
|
||||
r.operationID = "createSubmissionAdmin"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions-admin"
|
||||
r.args = args
|
||||
r.count = 0
|
||||
@@ -2593,6 +2896,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = GetSubmissionOperation
|
||||
r.summary = "Retrieve map with ID"
|
||||
r.operationID = "getSubmission"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2629,6 +2933,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ListSubmissionAuditEventsOperation
|
||||
r.summary = "Retrieve a list of audit events"
|
||||
r.operationID = "listSubmissionAuditEvents"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/audit-events"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2665,6 +2970,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = CreateSubmissionAuditCommentOperation
|
||||
r.summary = "Post a comment to the audit log"
|
||||
r.operationID = "createSubmissionAuditComment"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/comment"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2689,6 +2995,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = SetSubmissionCompletedOperation
|
||||
r.summary = "Called by maptest when a player completes the map"
|
||||
r.operationID = "setSubmissionCompleted"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/completed"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2715,6 +3022,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = UpdateSubmissionModelOperation
|
||||
r.summary = "Update model following role restrictions"
|
||||
r.operationID = "updateSubmissionModel"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/model"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2763,6 +3071,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionSubmissionRejectOperation
|
||||
r.summary = "Role Reviewer changes status from Submitted -> Rejected"
|
||||
r.operationID = "actionSubmissionReject"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/reject"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2787,6 +3096,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionSubmissionRequestChangesOperation
|
||||
r.summary = "Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested"
|
||||
r.operationID = "actionSubmissionRequestChanges"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/request-changes"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2823,6 +3133,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionSubmissionResetSubmittingOperation
|
||||
r.summary = "Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction"
|
||||
r.operationID = "actionSubmissionResetSubmitting"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/reset-submitting"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2847,6 +3158,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionSubmissionValidatedOperation
|
||||
r.summary = "Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> Validated"
|
||||
r.operationID = "actionSubmissionValidated"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/reset-uploading"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2871,6 +3183,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionSubmissionAcceptedOperation
|
||||
r.summary = "Role Reviewer manually resets validating softlock and changes status from Validating -> Accepted"
|
||||
r.operationID = "actionSubmissionAccepted"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/reset-validating"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2897,6 +3210,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionSubmissionRetryValidateOperation
|
||||
r.summary = "Role Reviewer re-runs validation and changes status from Accepted -> Validating"
|
||||
r.operationID = "actionSubmissionRetryValidate"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/retry-validate"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2921,6 +3235,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionSubmissionRevokeOperation
|
||||
r.summary = "Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction"
|
||||
r.operationID = "actionSubmissionRevoke"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/revoke"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2958,6 +3273,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionSubmissionTriggerSubmitOperation
|
||||
r.summary = "Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting"
|
||||
r.operationID = "actionSubmissionTriggerSubmit"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-submit"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -2982,6 +3298,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionSubmissionTriggerSubmitUncheckedOperation
|
||||
r.summary = "Role Reviewer changes status from ChangesRequested -> Submitting"
|
||||
r.operationID = "actionSubmissionTriggerSubmitUnchecked"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-submit-unchecked"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -3008,6 +3325,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionSubmissionTriggerUploadOperation
|
||||
r.summary = "Role SubmissionUpload changes status from Validated -> Uploading"
|
||||
r.operationID = "actionSubmissionTriggerUpload"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-upload"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
@@ -3032,6 +3350,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
r.name = ActionSubmissionTriggerValidateOperation
|
||||
r.summary = "Role Reviewer triggers validation and changes status from Submitted -> Validating"
|
||||
r.operationID = "actionSubmissionTriggerValidate"
|
||||
r.operationGroup = ""
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-validate"
|
||||
r.args = args
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/go-faster/jx"
|
||||
)
|
||||
|
||||
@@ -192,6 +193,254 @@ func (s *AuditEventEventData) init() AuditEventEventData {
|
||||
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 {
|
||||
APIKey string
|
||||
Roles []string
|
||||
@@ -324,6 +573,132 @@ func (s *ErrorStatusCode) SetResponse(val Error) {
|
||||
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
|
||||
type Map struct {
|
||||
ID int64 `json:"ID"`
|
||||
@@ -777,6 +1152,328 @@ func (s *OperationID) SetOperationID(val int32) {
|
||||
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.
|
||||
func NewOptInt32(v int32) OptInt32 {
|
||||
return OptInt32{
|
||||
@@ -1302,6 +1999,83 @@ type SetMapfixCompletedNoContent struct{}
|
||||
// SetSubmissionCompletedNoContent is response for SetSubmissionCompleted operation.
|
||||
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
|
||||
type Submission struct {
|
||||
ID int64 `json:"ID"`
|
||||
@@ -1534,6 +2308,23 @@ func (s *Submissions) SetSubmissions(val []Submission) {
|
||||
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.
|
||||
type UpdateMapfixModelNoContent struct{}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
)
|
||||
|
||||
@@ -75,6 +74,7 @@ var operationRolesCookieAuth = map[string][]string{
|
||||
SessionValidateOperation: []string{},
|
||||
SetMapfixCompletedOperation: []string{},
|
||||
SetSubmissionCompletedOperation: []string{},
|
||||
UpdateMapfixDescriptionOperation: []string{},
|
||||
UpdateMapfixModelOperation: []string{},
|
||||
UpdateScriptOperation: []string{},
|
||||
UpdateScriptPolicyOperation: []string{},
|
||||
|
||||
@@ -155,6 +155,24 @@ type Handler interface {
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/status/reset-uploading
|
||||
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.
|
||||
//
|
||||
// Trigger the validator to create a mapfix.
|
||||
@@ -215,6 +233,12 @@ type Handler interface {
|
||||
//
|
||||
// GET /maps/{MapID}/download
|
||||
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.
|
||||
//
|
||||
// Retrieve map with ID.
|
||||
@@ -245,12 +269,24 @@ type Handler interface {
|
||||
//
|
||||
// GET /script-policy/{ScriptPolicyID}
|
||||
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.
|
||||
//
|
||||
// Retrieve map with ID.
|
||||
//
|
||||
// GET /submissions/{SubmissionID}
|
||||
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.
|
||||
//
|
||||
// Retrieve a list of audit events.
|
||||
@@ -329,6 +365,12 @@ type Handler interface {
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/completed
|
||||
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.
|
||||
//
|
||||
// Update model following role restrictions.
|
||||
|
||||
@@ -232,6 +232,33 @@ func (UnimplementedHandler) ActionSubmissionValidated(ctx context.Context, param
|
||||
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.
|
||||
//
|
||||
// Trigger the validator to create a mapfix.
|
||||
@@ -322,6 +349,15 @@ func (UnimplementedHandler) DownloadMapAsset(ctx context.Context, params Downloa
|
||||
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.
|
||||
//
|
||||
// Retrieve map with ID.
|
||||
@@ -367,6 +403,15 @@ func (UnimplementedHandler) GetScriptPolicy(ctx context.Context, params GetScrip
|
||||
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.
|
||||
//
|
||||
// Retrieve map with ID.
|
||||
@@ -376,6 +421,15 @@ func (UnimplementedHandler) GetSubmission(ctx context.Context, params GetSubmiss
|
||||
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.
|
||||
//
|
||||
// Retrieve a list of audit events.
|
||||
@@ -493,6 +547,15 @@ func (UnimplementedHandler) SetSubmissionCompleted(ctx context.Context, params S
|
||||
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.
|
||||
//
|
||||
// Update model following role restrictions.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ import (
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/validator_controller"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/web_api"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/redis/go-redis/v9"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
"google.golang.org/grpc"
|
||||
@@ -102,6 +103,24 @@ func NewServeCommand() *cli.Command {
|
||||
EnvVars: []string{"RBX_API_KEY"},
|
||||
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")
|
||||
}
|
||||
|
||||
// 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
|
||||
conn, err := grpc.Dial(ctx.String("data-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
@@ -139,13 +176,15 @@ func serve(ctx *cli.Context) error {
|
||||
js,
|
||||
maps.NewMapsServiceClient(conn),
|
||||
users.NewUsersServiceClient(conn),
|
||||
robloxClient,
|
||||
redisClient,
|
||||
)
|
||||
|
||||
svc_external := web_api.NewService(
|
||||
&svc_inner,
|
||||
roblox.Client{
|
||||
HttpClient: http.DefaultClient,
|
||||
ApiKey: ctx.String("rbx-api-key"),
|
||||
ApiKey: ctx.String("rbx-api-key"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -31,9 +31,12 @@ type CheckSubmissionRequest struct{
|
||||
}
|
||||
|
||||
type CheckMapfixRequest struct{
|
||||
MapfixID int64
|
||||
ModelID uint64
|
||||
SkipChecks bool
|
||||
MapfixID int64
|
||||
ModelID uint64
|
||||
SkipChecks bool
|
||||
DisplayName string
|
||||
Creator string
|
||||
GameID uint32
|
||||
}
|
||||
|
||||
type ValidateSubmissionRequest struct {
|
||||
|
||||
@@ -17,7 +17,7 @@ type ScriptPolicy struct {
|
||||
// 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.
|
||||
// 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)
|
||||
// or verbatim source (ScriptPolicyAllowed)
|
||||
// or 0 (other)
|
||||
|
||||
@@ -26,7 +26,7 @@ func HashParse(hash string) (uint64, error){
|
||||
type Script struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
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
|
||||
ResourceType ResourceType // is this a submission or is it a mapfix
|
||||
ResourceID int64 // which submission / mapfix did this script first appear in
|
||||
|
||||
160
pkg/roblox/thumbnails.go
Normal file
160
pkg/roblox/thumbnails.go
Normal 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
72
pkg/roblox/users.go
Normal 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
|
||||
}
|
||||
@@ -33,14 +33,20 @@ func (svc *Service) NatsCreateMapfix(
|
||||
}
|
||||
|
||||
func (svc *Service) NatsCheckMapfix(
|
||||
MapfixID int64,
|
||||
ModelID uint64,
|
||||
SkipChecks bool,
|
||||
MapfixID int64,
|
||||
ModelID uint64,
|
||||
SkipChecks bool,
|
||||
DisplayName string,
|
||||
Creator string,
|
||||
GameID uint32,
|
||||
) error {
|
||||
validate_request := model.CheckMapfixRequest{
|
||||
MapfixID: MapfixID,
|
||||
ModelID: ModelID,
|
||||
SkipChecks: SkipChecks,
|
||||
DisplayName: DisplayName,
|
||||
Creator: Creator,
|
||||
GameID: GameID,
|
||||
}
|
||||
|
||||
j, err := json.Marshal(validate_request)
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.itzana.me/strafesnet/go-grpc/maps"
|
||||
"git.itzana.me/strafesnet/go-grpc/users"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db datastore.Datastore
|
||||
nats nats.JetStreamContext
|
||||
maps maps.MapsServiceClient
|
||||
users users.UsersServiceClient
|
||||
db datastore.Datastore
|
||||
nats nats.JetStreamContext
|
||||
maps maps.MapsServiceClient
|
||||
users users.UsersServiceClient
|
||||
thumbnailService *ThumbnailService
|
||||
}
|
||||
|
||||
func NewService(
|
||||
@@ -19,11 +24,44 @@ func NewService(
|
||||
nats nats.JetStreamContext,
|
||||
maps maps.MapsServiceClient,
|
||||
users users.UsersServiceClient,
|
||||
robloxClient *roblox.Client,
|
||||
redisClient *redis.Client,
|
||||
) Service {
|
||||
return Service{
|
||||
db: db,
|
||||
nats: nats,
|
||||
maps: maps,
|
||||
users: users,
|
||||
db: db,
|
||||
nats: nats,
|
||||
maps: maps,
|
||||
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
218
pkg/service/thumbnails.go
Normal 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
108
pkg/service/users.go
Normal 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)
|
||||
}
|
||||
@@ -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.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
@@ -406,7 +448,12 @@ func (svc *Service) ActionMapfixRequestChanges(ctx context.Context, params api.A
|
||||
target_status := model.MapfixStatusChangesRequested
|
||||
update := service.NewMapfixUpdate()
|
||||
update.SetStatusID(target_status)
|
||||
allow_statuses := []model.MapfixStatus{model.MapfixStatusValidated, model.MapfixStatusAcceptedUnvalidated, model.MapfixStatusSubmitted}
|
||||
allow_statuses := []model.MapfixStatus{
|
||||
model.MapfixStatusUploaded,
|
||||
model.MapfixStatusValidated,
|
||||
model.MapfixStatusAcceptedUnvalidated,
|
||||
model.MapfixStatusSubmitted,
|
||||
}
|
||||
err = svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -491,13 +538,13 @@ func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.Ac
|
||||
return ErrUserInfo
|
||||
}
|
||||
|
||||
// read mapfix (this could be done with a transaction WHERE clause)
|
||||
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
|
||||
userId, err := userInfo.GetUserID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userId, err := userInfo.GetUserID()
|
||||
// read mapfix (this could be done with a transaction WHERE clause)
|
||||
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -508,6 +555,12 @@ func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.Ac
|
||||
return ErrPermissionDeniedNotSubmitter
|
||||
}
|
||||
|
||||
// read map to get current DisplayName and such
|
||||
target_map, err := svc.inner.GetMap(ctx, int64(mapfix.TargetAssetID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// transaction
|
||||
target_status := model.MapfixStatusSubmitting
|
||||
update := service.NewMapfixUpdate()
|
||||
@@ -522,6 +575,9 @@ func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.Ac
|
||||
mapfix.ID,
|
||||
mapfix.AssetID,
|
||||
false,
|
||||
target_map.DisplayName,
|
||||
target_map.Creator,
|
||||
target_map.GameID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -553,13 +609,13 @@ func (svc *Service) ActionMapfixTriggerSubmitUnchecked(ctx context.Context, para
|
||||
return ErrUserInfo
|
||||
}
|
||||
|
||||
// read mapfix (this could be done with a transaction WHERE clause)
|
||||
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
|
||||
userId, err := userInfo.GetUserID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userId, err := userInfo.GetUserID()
|
||||
// read mapfix (this could be done with a transaction WHERE clause)
|
||||
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -579,6 +635,12 @@ func (svc *Service) ActionMapfixTriggerSubmitUnchecked(ctx context.Context, para
|
||||
return ErrPermissionDeniedNeedRoleMapfixReview
|
||||
}
|
||||
|
||||
// read map to get current DisplayName and such
|
||||
target_map, err := svc.inner.GetMap(ctx, int64(mapfix.TargetAssetID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// transaction
|
||||
target_status := model.MapfixStatusSubmitting
|
||||
update := service.NewMapfixUpdate()
|
||||
@@ -593,6 +655,9 @@ func (svc *Service) ActionMapfixTriggerSubmitUnchecked(ctx context.Context, para
|
||||
mapfix.ID,
|
||||
mapfix.AssetID,
|
||||
true,
|
||||
target_map.DisplayName,
|
||||
target_map.Creator,
|
||||
target_map.GameID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -36,10 +36,28 @@ func (svc *Service) CreateScript(ctx context.Context, req *api.ScriptCreate) (*a
|
||||
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{
|
||||
ID: 0,
|
||||
Name: req.Name,
|
||||
Hash: int64(model.HashSource(req.Source)),
|
||||
Hash: hash,
|
||||
Source: req.Source,
|
||||
ResourceType: model.ResourceType(req.ResourceType),
|
||||
ResourceID: req.ResourceID.Or(0),
|
||||
|
||||
105
pkg/web_api/stats.go
Normal file
105
pkg/web_api/stats.go
Normal 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
|
||||
}
|
||||
@@ -437,7 +437,12 @@ func (svc *Service) ActionSubmissionRequestChanges(ctx context.Context, params a
|
||||
target_status := model.SubmissionStatusChangesRequested
|
||||
update := service.NewSubmissionUpdate()
|
||||
update.SetStatusID(target_status)
|
||||
allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusValidated, model.SubmissionStatusAcceptedUnvalidated, model.SubmissionStatusSubmitted}
|
||||
allowed_statuses := []model.SubmissionStatus{
|
||||
model.SubmissionStatusUploaded,
|
||||
model.SubmissionStatusValidated,
|
||||
model.SubmissionStatusAcceptedUnvalidated,
|
||||
model.SubmissionStatusSubmitted,
|
||||
}
|
||||
err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
135
pkg/web_api/thumbnails.go
Normal file
135
pkg/web_api/thumbnails.go
Normal 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
33
pkg/web_api/users.go
Normal 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
|
||||
}
|
||||
@@ -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 ScriptPolicySingleItemError=SingleItemError<Vec<ScriptPolicyID>>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct UrlAndBody{
|
||||
pub url:url::Url,
|
||||
@@ -76,7 +75,7 @@ pub enum GameID{
|
||||
FlyTrials=5,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct CreateMapfixRequest<'a>{
|
||||
pub OperationID:OperationID,
|
||||
@@ -89,13 +88,13 @@ pub struct CreateMapfixRequest<'a>{
|
||||
pub TargetAssetID:u64,
|
||||
pub Description:&'a str,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct MapfixIDResponse{
|
||||
pub MapfixID:MapfixID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct CreateSubmissionRequest<'a>{
|
||||
pub OperationID:OperationID,
|
||||
@@ -108,7 +107,7 @@ pub struct CreateSubmissionRequest<'a>{
|
||||
pub Status:u32,
|
||||
pub Roles:u32,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct SubmissionIDResponse{
|
||||
pub SubmissionID:SubmissionID,
|
||||
@@ -127,11 +126,11 @@ pub enum ResourceType{
|
||||
Submission=2,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct GetScriptRequest{
|
||||
pub ScriptID:ScriptID,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct GetScriptsRequest<'a>{
|
||||
pub Page:u32,
|
||||
@@ -151,7 +150,7 @@ pub struct GetScriptsRequest<'a>{
|
||||
pub struct HashRequest<'a>{
|
||||
pub hash:&'a str,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct ScriptResponse{
|
||||
pub ID:ScriptID,
|
||||
@@ -161,7 +160,7 @@ pub struct ScriptResponse{
|
||||
pub ResourceType:ResourceType,
|
||||
pub ResourceID:ResourceID,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct CreateScriptRequest<'a>{
|
||||
pub Name:&'a str,
|
||||
@@ -170,7 +169,7 @@ pub struct CreateScriptRequest<'a>{
|
||||
#[serde(skip_serializing_if="Option::is_none")]
|
||||
pub ResourceID:Option<ResourceID>,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct ScriptIDResponse{
|
||||
pub ScriptID:ScriptID,
|
||||
@@ -186,11 +185,11 @@ pub enum Policy{
|
||||
Replace=4,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct GetScriptPolicyRequest{
|
||||
pub ScriptPolicyID:ScriptPolicyID,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct GetScriptPoliciesRequest<'a>{
|
||||
pub Page:u32,
|
||||
@@ -202,7 +201,7 @@ pub struct GetScriptPoliciesRequest<'a>{
|
||||
#[serde(skip_serializing_if="Option::is_none")]
|
||||
pub Policy:Option<Policy>,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct ScriptPolicyResponse{
|
||||
pub ID:ScriptPolicyID,
|
||||
@@ -210,20 +209,20 @@ pub struct ScriptPolicyResponse{
|
||||
pub ToScriptID:ScriptID,
|
||||
pub Policy:Policy
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct CreateScriptPolicyRequest{
|
||||
pub FromScriptID:ScriptID,
|
||||
pub ToScriptID:ScriptID,
|
||||
pub Policy:Policy,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct ScriptPolicyIDResponse{
|
||||
pub ScriptPolicyID:ScriptPolicyID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct UpdateScriptPolicyRequest{
|
||||
pub ID:ScriptPolicyID,
|
||||
@@ -235,7 +234,7 @@ pub struct UpdateScriptPolicyRequest{
|
||||
pub Policy:Option<Policy>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct UpdateSubmissionModelRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
@@ -276,7 +275,7 @@ pub enum MapfixStatus{
|
||||
Released=10,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct GetMapfixesRequest<'a>{
|
||||
pub Page:u32,
|
||||
@@ -292,7 +291,7 @@ pub struct GetMapfixesRequest<'a>{
|
||||
pub StatusID:Option<MapfixStatus>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize,serde::Deserialize)]
|
||||
pub struct MapfixResponse{
|
||||
pub ID:MapfixID,
|
||||
@@ -312,7 +311,7 @@ pub struct MapfixResponse{
|
||||
pub Description:String,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct MapfixesResponse{
|
||||
pub Total:u64,
|
||||
@@ -342,7 +341,7 @@ pub enum SubmissionStatus{
|
||||
Released=10,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct GetSubmissionsRequest<'a>{
|
||||
pub Page:u32,
|
||||
@@ -358,7 +357,7 @@ pub struct GetSubmissionsRequest<'a>{
|
||||
pub StatusID:Option<SubmissionStatus>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct SubmissionResponse{
|
||||
pub ID:SubmissionID,
|
||||
@@ -376,14 +375,14 @@ pub struct SubmissionResponse{
|
||||
pub StatusID:SubmissionStatus,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct SubmissionsResponse{
|
||||
pub Total:u64,
|
||||
pub Submissions:Vec<SubmissionResponse>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct GetMapsRequest<'a>{
|
||||
pub Page:u32,
|
||||
@@ -394,7 +393,7 @@ pub struct GetMapsRequest<'a>{
|
||||
pub GameID:Option<GameID>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct MapResponse{
|
||||
pub ID:i64,
|
||||
@@ -404,7 +403,7 @@ pub struct MapResponse{
|
||||
pub Date:i64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct GetMapfixAuditEventsRequest{
|
||||
pub Page:u32,
|
||||
@@ -412,7 +411,7 @@ pub struct GetMapfixAuditEventsRequest{
|
||||
pub MapfixID:i64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct GetSubmissionAuditEventsRequest{
|
||||
pub Page:u32,
|
||||
@@ -420,7 +419,6 @@ pub struct GetSubmissionAuditEventsRequest{
|
||||
pub SubmissionID:i64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde_repr::Deserialize_repr)]
|
||||
#[repr(u32)]
|
||||
pub enum AuditEventType{
|
||||
@@ -475,7 +473,6 @@ pub struct AuditEventCheckList{
|
||||
pub check_list:Vec<AuditEventCheck>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum AuditEventData{
|
||||
Action(AuditEventAction),
|
||||
@@ -491,7 +488,7 @@ pub enum AuditEventData{
|
||||
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,serde::Serialize,serde::Deserialize)]
|
||||
pub struct AuditEventID(pub(crate)i64);
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct AuditEventReponse{
|
||||
pub ID:AuditEventID,
|
||||
@@ -518,7 +515,7 @@ impl AuditEventReponse{
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct Check{
|
||||
pub Name:&'static str,
|
||||
@@ -526,7 +523,7 @@ pub struct Check{
|
||||
pub Passed:bool,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionSubmissionSubmittedRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
@@ -536,33 +533,33 @@ pub struct ActionSubmissionSubmittedRequest{
|
||||
pub GameID:GameID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionSubmissionRequestChangesRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionSubmissionUploadedRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
pub UploadedAssetID:u64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionSubmissionAcceptedRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct CreateSubmissionAuditErrorRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
pub ErrorMessage:String,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct CreateSubmissionAuditCheckListRequest<'a>{
|
||||
pub SubmissionID:SubmissionID,
|
||||
@@ -580,7 +577,7 @@ impl SubmissionID{
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct UpdateMapfixModelRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
@@ -588,7 +585,7 @@ pub struct UpdateMapfixModelRequest{
|
||||
pub ModelVersion:u64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionMapfixSubmittedRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
@@ -598,32 +595,32 @@ pub struct ActionMapfixSubmittedRequest{
|
||||
pub GameID:GameID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionMapfixRequestChangesRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionMapfixUploadedRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionMapfixAcceptedRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct CreateMapfixAuditErrorRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
pub ErrorMessage:String,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct CreateMapfixAuditCheckListRequest<'a>{
|
||||
pub MapfixID:MapfixID,
|
||||
@@ -641,7 +638,7 @@ impl MapfixID{
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionOperationFailedRequest{
|
||||
pub OperationID:OperationID,
|
||||
@@ -668,7 +665,7 @@ impl Resource{
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct ReleaseInfo{
|
||||
pub SubmissionID:SubmissionID,
|
||||
@@ -678,7 +675,7 @@ pub struct ReleaseInfo{
|
||||
pub struct ReleaseRequest<'a>{
|
||||
pub schedule:&'a [ReleaseInfo],
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct OperationIDResponse{
|
||||
pub OperationID:OperationID,
|
||||
|
||||
@@ -14,6 +14,7 @@ rbx_xml = "2.0.0"
|
||||
regex = { version = "1.11.3", default-features = false }
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
serde_repr = "0.1.19"
|
||||
siphasher = "1.0.1"
|
||||
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "signal"] }
|
||||
heck = "0.5.0"
|
||||
|
||||
@@ -6,7 +6,7 @@ use heck::{ToSnakeCase,ToTitleCase};
|
||||
use rbx_dom_weak::Instance;
|
||||
use rust_grpc::validator::Check;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ModelInfoDownload(rbx_asset::cloud::GetError),
|
||||
@@ -33,29 +33,6 @@ macro_rules! lazy_regex{
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
pub struct CheckRequest{
|
||||
ModelID:u64,
|
||||
SkipChecks:bool,
|
||||
}
|
||||
|
||||
impl From<crate::nats_types::CheckMapfixRequest> for CheckRequest{
|
||||
fn from(value:crate::nats_types::CheckMapfixRequest)->Self{
|
||||
Self{
|
||||
ModelID:value.ModelID,
|
||||
SkipChecks:value.SkipChecks,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<crate::nats_types::CheckSubmissionRequest> for CheckRequest{
|
||||
fn from(value:crate::nats_types::CheckSubmissionRequest)->Self{
|
||||
Self{
|
||||
ModelID:value.ModelID,
|
||||
SkipChecks:value.SkipChecks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,Ord,PartialOrd)]
|
||||
struct ModeID(u64);
|
||||
impl ModeID{
|
||||
@@ -79,7 +56,7 @@ struct ModeElement{
|
||||
zone:Zone,
|
||||
mode_id:ModeID,
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
pub enum IDParseError{
|
||||
NoCaptures,
|
||||
ParseInt(core::num::ParseIntError),
|
||||
@@ -323,26 +300,25 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d
|
||||
}
|
||||
}
|
||||
|
||||
// check if an observed string matches an expected string
|
||||
pub struct StringCheck<'a,T,Str>(Result<T,StringCheckContext<'a,Str>>);
|
||||
pub struct StringCheckContext<'a,Str>{
|
||||
observed:&'a str,
|
||||
expected:Str,
|
||||
// check if an observed value matches an expected value
|
||||
pub struct EqualityCheck<Obs,Exp>{
|
||||
observed:Obs,
|
||||
expected:Exp,
|
||||
}
|
||||
impl<'a,Str> StringCheckContext<'a,Str>
|
||||
impl<Obs,Exp> EqualityCheck<Obs,Exp>
|
||||
where
|
||||
&'a str:PartialEq<Str>,
|
||||
Obs:PartialEq<Exp>,
|
||||
{
|
||||
/// Compute the StringCheck, passing through the provided value on success.
|
||||
fn check<T>(self,value:T)->StringCheck<'a,T,Str>{
|
||||
fn check<T>(self,value:T)->Result<T,Self>{
|
||||
if self.observed==self.expected{
|
||||
StringCheck(Ok(value))
|
||||
Ok(value)
|
||||
}else{
|
||||
StringCheck(Err(self))
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<Str:std::fmt::Display> std::fmt::Display for StringCheckContext<'_,Str>{
|
||||
impl<Obs:std::fmt::Display,Exp:std::fmt::Display> std::fmt::Display for EqualityCheck<Obs,Exp>{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"expected: {}, observed: {}",self.expected,self.observed)
|
||||
}
|
||||
@@ -442,7 +418,7 @@ pub struct MapInfoOwned{
|
||||
pub creator:String,
|
||||
pub game_id:GameID,
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum IntoMapInfoOwnedError{
|
||||
DisplayName(StringValueError),
|
||||
@@ -464,22 +440,164 @@ impl TryFrom<MapInfo<'_>> for MapInfoOwned{
|
||||
struct Exists;
|
||||
struct Absent;
|
||||
|
||||
enum DisplayNameError<'a>{
|
||||
TitleCase(EqualityCheck<&'a str,String>),
|
||||
CannotChange(EqualityCheck<&'a str,String>),
|
||||
Empty(StringEmpty),
|
||||
TooLong(usize),
|
||||
StringValue(StringValueError),
|
||||
}
|
||||
|
||||
enum CreatorError<'a>{
|
||||
CannotChange(EqualityCheck<&'a str,String>),
|
||||
Empty(StringEmpty),
|
||||
TooLong(usize),
|
||||
StringValue(StringValueError),
|
||||
}
|
||||
|
||||
enum GameIDError{
|
||||
CannotChange(EqualityCheck<GameID,GameID>),
|
||||
Parse(ParseGameIDError),
|
||||
}
|
||||
|
||||
pub struct CheckedMapInfo<'a>{
|
||||
display_name:Result<&'a str,DisplayNameError<'a>>,
|
||||
creator:Result<&'a str,CreatorError<'a>>,
|
||||
game_id:Result<GameID,GameIDError>,
|
||||
}
|
||||
|
||||
pub struct NoMapInfo;
|
||||
impl CheckModelInfo for NoMapInfo{
|
||||
fn check<'a>(self,map_info:MapInfo<'a>)->CheckedMapInfo<'a>{
|
||||
fn check_display_name(display_name:Result<&str,StringValueError>)->Result<&str,DisplayNameError<'_>>{
|
||||
// DisplayName StringValue can be missing or whatever
|
||||
let display_name=display_name.map_err(DisplayNameError::StringValue)?;
|
||||
|
||||
// DisplayName cannot be ""
|
||||
let display_name=check_empty(display_name).map_err(DisplayNameError::Empty)?;
|
||||
|
||||
// DisplayName cannot exceed 50 characters
|
||||
if 50<display_name.len(){
|
||||
return Err(DisplayNameError::TooLong(display_name.len()));
|
||||
}
|
||||
|
||||
// Check title case
|
||||
let display_name=EqualityCheck{
|
||||
observed:display_name,
|
||||
expected:display_name.to_title_case(),
|
||||
}.check(display_name).map_err(DisplayNameError::TitleCase)?;
|
||||
|
||||
Ok(display_name)
|
||||
}
|
||||
fn check_creator(creator:Result<&str,StringValueError>)->Result<&str,CreatorError<'_>>{
|
||||
// Creator StringValue can be missing or whatever
|
||||
let creator=creator.map_err(CreatorError::StringValue)?;
|
||||
|
||||
// Creator cannot be ""
|
||||
let creator=check_empty(creator).map_err(CreatorError::Empty)?;
|
||||
|
||||
// Creator cannot exceed 50 characters
|
||||
if 50<creator.len(){
|
||||
return Err(CreatorError::TooLong(creator.len()));
|
||||
}
|
||||
|
||||
Ok(creator)
|
||||
}
|
||||
fn check_game_id(game_id:Result<GameID,ParseGameIDError>)->Result<GameID,GameIDError>{
|
||||
// Creator StringValue can be missing or whatever
|
||||
let game_id=game_id.map_err(GameIDError::Parse)?;
|
||||
|
||||
Ok(game_id)
|
||||
}
|
||||
|
||||
// Check display name is not empty and has title case
|
||||
let display_name=check_display_name(map_info.display_name);
|
||||
|
||||
// Check Creator is not empty
|
||||
let creator=check_creator(map_info.creator);
|
||||
|
||||
// Check GameID (model name was prefixed with bhop_ surf_ etc)
|
||||
let game_id=check_game_id(map_info.game_id);
|
||||
|
||||
CheckedMapInfo{
|
||||
display_name,
|
||||
creator,
|
||||
game_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl CheckModelInfo for MapInfoOwned{
|
||||
fn check<'a>(self,map_info:MapInfo<'a>)->CheckedMapInfo<'a>{
|
||||
fn check_display_name(display_name:Result<&str,StringValueError>,target_display_name:String)->Result<&str,DisplayNameError<'_>>{
|
||||
// DisplayName StringValue can be missing or whatever
|
||||
let display_name=display_name.map_err(DisplayNameError::StringValue)?;
|
||||
|
||||
// Mapfix cannot change display name
|
||||
let display_name=EqualityCheck{
|
||||
observed:display_name,
|
||||
expected:target_display_name,
|
||||
}.check(display_name).map_err(DisplayNameError::CannotChange)?;
|
||||
|
||||
Ok(display_name)
|
||||
}
|
||||
fn check_creator(creator:Result<&str,StringValueError>,target_creator:String)->Result<&str,CreatorError<'_>>{
|
||||
// Creator StringValue can be missing or whatever
|
||||
let creator=creator.map_err(CreatorError::StringValue)?;
|
||||
|
||||
// Mapfix cannot change creator
|
||||
let creator=EqualityCheck{
|
||||
observed:creator,
|
||||
expected:target_creator,
|
||||
}.check(creator).map_err(CreatorError::CannotChange)?;
|
||||
|
||||
Ok(creator)
|
||||
}
|
||||
fn check_game_id(game_id:Result<GameID,ParseGameIDError>,target_game_id:GameID)->Result<GameID,GameIDError>{
|
||||
// Creator StringValue can be missing or whatever
|
||||
let game_id=game_id.map_err(GameIDError::Parse)?;
|
||||
|
||||
// Mapfix cannot change game_id
|
||||
let game_id=EqualityCheck{
|
||||
observed:game_id,
|
||||
expected:target_game_id,
|
||||
}.check(game_id).map_err(GameIDError::CannotChange)?;
|
||||
|
||||
Ok(game_id)
|
||||
}
|
||||
|
||||
// Check display name is not empty and has title case
|
||||
let display_name=check_display_name(map_info.display_name,self.display_name);
|
||||
|
||||
// Check Creator is not empty
|
||||
let creator=check_creator(map_info.creator,self.creator);
|
||||
|
||||
// Check GameID (model name was prefixed with bhop_ surf_ etc)
|
||||
let game_id=check_game_id(map_info.game_id,self.game_id);
|
||||
|
||||
CheckedMapInfo{
|
||||
display_name,
|
||||
creator,
|
||||
game_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of every map check.
|
||||
struct MapCheck<'a>{
|
||||
// === METADATA CHECKS ===
|
||||
// The root must be of class Model
|
||||
model_class:StringCheck<'a,(),&'static str>,
|
||||
model_class:Result<(),EqualityCheck<&'a str,&'static str>>,
|
||||
// Model's name must be in snake case
|
||||
model_name:StringCheck<'a,(),String>,
|
||||
model_name:Result<(),EqualityCheck<&'a str,String>>,
|
||||
// Map must have a StringValue named DisplayName.
|
||||
// Value must not be empty, must be in title case.
|
||||
display_name:Result<Result<StringCheck<'a,&'a str,String>,StringEmpty>,StringValueError>,
|
||||
display_name:Result<&'a str,DisplayNameError<'a>>,
|
||||
// Map must have a StringValue named Creator.
|
||||
// Value must not be empty.
|
||||
creator:Result<Result<&'a str,StringEmpty>,StringValueError>,
|
||||
creator:Result<&'a str,CreatorError<'a>>,
|
||||
// The prefix of the model's name must match the game it was submitted for.
|
||||
// bhop_ for bhop, and surf_ for surf
|
||||
game_id:Result<GameID,ParseGameIDError>,
|
||||
game_id:Result<GameID,GameIDError>,
|
||||
|
||||
// === MODE CHECKS ===
|
||||
// MapStart must exist
|
||||
@@ -509,32 +627,24 @@ struct MapCheck<'a>{
|
||||
}
|
||||
|
||||
impl<'a> ModelInfo<'a>{
|
||||
fn check(self)->MapCheck<'a>{
|
||||
fn check<I:CheckModelInfo>(self,model_info:I)->MapCheck<'a>{
|
||||
// Check class is exactly "Model"
|
||||
let model_class=StringCheckContext{
|
||||
let model_class=EqualityCheck{
|
||||
observed:self.model_class,
|
||||
expected:"Model",
|
||||
}.check(());
|
||||
|
||||
// Check model name is snake case
|
||||
let model_name=StringCheckContext{
|
||||
let model_name=EqualityCheck{
|
||||
observed:self.model_name,
|
||||
expected:self.model_name.to_snake_case(),
|
||||
}.check(());
|
||||
|
||||
// Check display name is not empty and has title case
|
||||
let display_name=self.map_info.display_name.map(|display_name|{
|
||||
check_empty(display_name).map(|display_name|StringCheckContext{
|
||||
observed:display_name,
|
||||
expected:display_name.to_title_case(),
|
||||
}.check(display_name))
|
||||
});
|
||||
|
||||
// Check Creator is not empty
|
||||
let creator=self.map_info.creator.map(check_empty);
|
||||
|
||||
// Check GameID (model name was prefixed with bhop_ surf_ etc)
|
||||
let game_id=self.map_info.game_id;
|
||||
let CheckedMapInfo{
|
||||
display_name,
|
||||
creator,
|
||||
game_id,
|
||||
}=model_info.check(self.map_info);
|
||||
|
||||
// MapStart must exist
|
||||
let mapstart=if self.counts.mode_start_counts.contains_key(&ModeID::MAIN){
|
||||
@@ -630,10 +740,10 @@ impl MapCheck<'_>{
|
||||
fn result(self)->Result<MapInfoOwned,Result<MapCheckList,serde_json::Error>>{
|
||||
match self{
|
||||
MapCheck{
|
||||
model_class:StringCheck(Ok(())),
|
||||
model_name:StringCheck(Ok(())),
|
||||
display_name:Ok(Ok(StringCheck(Ok(display_name)))),
|
||||
creator:Ok(Ok(creator)),
|
||||
model_class:Ok(()),
|
||||
model_name:Ok(()),
|
||||
display_name:Ok(display_name),
|
||||
creator:Ok(creator),
|
||||
game_id:Ok(game_id),
|
||||
mapstart:Ok(Exists),
|
||||
mode_start_counts:DuplicateCheck(Ok(())),
|
||||
@@ -737,31 +847,32 @@ macro_rules! summary_format{
|
||||
impl MapCheck<'_>{
|
||||
fn itemize(&self)->Result<MapCheckList,serde_json::Error>{
|
||||
let model_class=match &self.model_class{
|
||||
StringCheck(Ok(()))=>passed!("ModelClass"),
|
||||
StringCheck(Err(context))=>summary_format!("ModelClass","Invalid model class: {context}"),
|
||||
Ok(())=>passed!("ModelClass"),
|
||||
Err(context)=>summary_format!("ModelClass","Invalid model class: {context}"),
|
||||
};
|
||||
let model_name=match &self.model_name{
|
||||
StringCheck(Ok(()))=>passed!("ModelName"),
|
||||
StringCheck(Err(context))=>summary_format!("ModelName","Model name must have snake_case: {context}"),
|
||||
Ok(())=>passed!("ModelName"),
|
||||
Err(context)=>summary_format!("ModelName","Model name must have snake_case: {context}"),
|
||||
};
|
||||
let display_name=match &self.display_name{
|
||||
Ok(Ok(StringCheck(Ok(_))))=>passed!("DisplayName"),
|
||||
Ok(Ok(StringCheck(Err(context))))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"),
|
||||
Ok(Err(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"),
|
||||
Err(StringValueError::ObjectNotFound)=>summary!("DisplayName","Missing DisplayName StringValue".to_owned()),
|
||||
Err(StringValueError::ValueNotSet)=>summary!("DisplayName","DisplayName Value not set".to_owned()),
|
||||
Err(StringValueError::NonStringValue)=>summary!("DisplayName","DisplayName Value is not a String".to_owned()),
|
||||
Ok(_)=>passed!("DisplayName"),
|
||||
Err(DisplayNameError::TitleCase(context))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"),
|
||||
Err(DisplayNameError::CannotChange(context))=>summary_format!("DisplayName","DisplayName cannot be changed: {context}"),
|
||||
Err(DisplayNameError::Empty(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"),
|
||||
Err(DisplayNameError::TooLong(context))=>summary_format!("DisplayName","DisplayName is too long: {context} characters (50 characters max)"),
|
||||
Err(DisplayNameError::StringValue(context))=>summary_format!("DisplayName","DisplayName StringValue: {context}"),
|
||||
};
|
||||
let creator=match &self.creator{
|
||||
Ok(Ok(_))=>passed!("Creator"),
|
||||
Ok(Err(context))=>summary_format!("Creator","Invalid Creator: {context}"),
|
||||
Err(StringValueError::ObjectNotFound)=>summary!("Creator","Missing Creator StringValue".to_owned()),
|
||||
Err(StringValueError::ValueNotSet)=>summary!("Creator","Creator Value not set".to_owned()),
|
||||
Err(StringValueError::NonStringValue)=>summary!("Creator","Creator Value is not a String".to_owned()),
|
||||
Ok(_)=>passed!("Creator"),
|
||||
Err(CreatorError::CannotChange(context))=>summary_format!("Creator","Creator cannot be changed: {context}"),
|
||||
Err(CreatorError::Empty(context))=>summary_format!("Creator","Invalid Creator: {context}"),
|
||||
Err(CreatorError::TooLong(context))=>summary_format!("Creator","Creator is too long: {context} characters (50 characters max)"),
|
||||
Err(CreatorError::StringValue(context))=>summary_format!("Creator","Creator StringValue: {context}"),
|
||||
};
|
||||
let game_id=match &self.game_id{
|
||||
Ok(_)=>passed!("GameID"),
|
||||
Err(ParseGameIDError)=>summary!("GameID","Model name must be prefixed with bhop_ surf_ or flytrials_".to_owned()),
|
||||
Err(GameIDError::CannotChange(context))=>summary_format!("GameID","GameID cannot be changed: {context}"),
|
||||
Err(GameIDError::Parse(ParseGameIDError))=>summary!("GameID","Model name must be prefixed with bhop_ surf_ or flytrials_".to_owned()),
|
||||
};
|
||||
let mapstart=match &self.mapstart{
|
||||
Ok(Exists)=>passed!("MapStart"),
|
||||
@@ -922,8 +1033,55 @@ pub struct CheckListAndVersion{
|
||||
pub version:u64,
|
||||
}
|
||||
|
||||
pub trait CheckModelInfo{
|
||||
fn check<'a>(self,map_info:MapInfo<'a>)->CheckedMapInfo<'a>;
|
||||
}
|
||||
|
||||
pub trait CheckSpecialization{
|
||||
type ModelInfo:CheckModelInfo;
|
||||
fn info(self)->(CheckRequest,Self::ModelInfo);
|
||||
}
|
||||
|
||||
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct CheckRequest{
|
||||
ModelID:u64,
|
||||
SkipChecks:bool,
|
||||
}
|
||||
|
||||
impl CheckSpecialization for crate::nats_types::CheckMapfixRequest{
|
||||
type ModelInfo=MapInfoOwned;
|
||||
fn info(self)->(CheckRequest,Self::ModelInfo) {
|
||||
(
|
||||
CheckRequest{
|
||||
ModelID:self.ModelID,
|
||||
SkipChecks:self.SkipChecks,
|
||||
},
|
||||
MapInfoOwned{
|
||||
display_name:self.DisplayName,
|
||||
creator:self.Creator,
|
||||
game_id:self.GameID,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
impl CheckSpecialization for crate::nats_types::CheckSubmissionRequest{
|
||||
type ModelInfo=NoMapInfo;
|
||||
fn info(self)->(CheckRequest,Self::ModelInfo) {
|
||||
(
|
||||
CheckRequest{
|
||||
ModelID:self.ModelID,
|
||||
SkipChecks:self.SkipChecks,
|
||||
},
|
||||
NoMapInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::message_handler::MessageHandler{
|
||||
pub async fn check_inner(&self,check_info:CheckRequest)->Result<CheckListAndVersion,Error>{
|
||||
pub async fn check_inner<R:CheckSpecialization>(&self,check_info:R)->Result<CheckListAndVersion,Error>{
|
||||
let (check_info,target_model_info)=check_info.info();
|
||||
|
||||
// discover asset creator and latest version
|
||||
let info=self.cloud_context.get_asset_info(
|
||||
rbx_asset::cloud::GetAssetLatestRequest{asset_id:check_info.ModelID}
|
||||
@@ -963,7 +1121,7 @@ impl crate::message_handler::MessageHandler{
|
||||
let model_info=get_model_info(&dom,model_instance);
|
||||
|
||||
// convert the model information into a structured report
|
||||
let map_check=model_info.check();
|
||||
let map_check=model_info.check(target_model_info);
|
||||
|
||||
// check the report, generate an error message if it fails the check
|
||||
let status=match map_check.result(){
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::check::CheckListAndVersion;
|
||||
use crate::nats_types::CheckMapfixRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
Check(crate::check::Error),
|
||||
@@ -17,7 +17,7 @@ impl std::error::Error for Error{}
|
||||
impl crate::message_handler::MessageHandler{
|
||||
pub async fn check_mapfix(&self,check_info:CheckMapfixRequest)->Result<(),Error>{
|
||||
let mapfix_id=check_info.MapfixID;
|
||||
let check_result=self.check_inner(check_info.into()).await;
|
||||
let check_result=self.check_inner(check_info).await;
|
||||
|
||||
// update the mapfix depending on the result
|
||||
match check_result{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::check::CheckListAndVersion;
|
||||
use crate::nats_types::CheckSubmissionRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
Check(crate::check::Error),
|
||||
@@ -17,7 +17,7 @@ impl std::error::Error for Error{}
|
||||
impl crate::message_handler::MessageHandler{
|
||||
pub async fn check_submission(&self,check_info:CheckSubmissionRequest)->Result<(),Error>{
|
||||
let submission_id=check_info.SubmissionID;
|
||||
let check_result=self.check_inner(check_info.into()).await;
|
||||
let check_result=self.check_inner(check_info).await;
|
||||
|
||||
// update the submission depending on the result
|
||||
match check_result{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::download::download_asset_version;
|
||||
use crate::rbx_util::{get_root_instance,get_mapinfo,read_dom,MapInfo,ReadDomError,GetRootInstanceError,GameID};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
CreatorTypeMustBeUser,
|
||||
@@ -17,11 +17,11 @@ impl std::fmt::Display for Error{
|
||||
}
|
||||
impl std::error::Error for Error{}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct CreateRequest{
|
||||
pub ModelID:u64,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct CreateResult{
|
||||
pub AssetOwner:u64,
|
||||
pub DisplayName:Option<String>,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::nats_types::CreateMapfixRequest;
|
||||
use crate::create::CreateRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
Create(crate::create::Error),
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::nats_types::CreateSubmissionRequest;
|
||||
use crate::create::CreateRequest;
|
||||
use crate::rbx_util::GameID;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
Create(crate::create::Error),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ModelLocationDownload(rbx_asset::cloud::GetError),
|
||||
|
||||
@@ -22,7 +22,6 @@ mod validator;
|
||||
mod validate_mapfix;
|
||||
mod validate_submission;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum StartupError{
|
||||
API(tonic::transport::Error),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum HandleMessageError{
|
||||
Messages(async_nats::jetstream::consumer::pull::MessagesError),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Requests are sent from maps-service to validator
|
||||
// Validation invokes the REST api to update the submissions
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CreateSubmissionRequest{
|
||||
// operation_id is passed back in the response message
|
||||
@@ -18,7 +18,7 @@ pub struct CreateSubmissionRequest{
|
||||
pub Roles:u32,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CreateMapfixRequest{
|
||||
pub OperationID:u32,
|
||||
@@ -27,7 +27,7 @@ pub struct CreateMapfixRequest{
|
||||
pub Description:String,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CheckSubmissionRequest{
|
||||
pub SubmissionID:u64,
|
||||
@@ -35,15 +35,19 @@ pub struct CheckSubmissionRequest{
|
||||
pub SkipChecks:bool,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CheckMapfixRequest{
|
||||
pub MapfixID:u64,
|
||||
pub ModelID:u64,
|
||||
pub SkipChecks:bool,
|
||||
// target map info
|
||||
pub DisplayName:String,
|
||||
pub Creator:String,
|
||||
pub GameID:crate::rbx_util::GameID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ValidateSubmissionRequest{
|
||||
// submission_id is passed back in the response message
|
||||
@@ -53,7 +57,7 @@ pub struct ValidateSubmissionRequest{
|
||||
pub ValidatedModelID:Option<u64>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ValidateMapfixRequest{
|
||||
// submission_id is passed back in the response message
|
||||
@@ -64,7 +68,7 @@ pub struct ValidateMapfixRequest{
|
||||
}
|
||||
|
||||
// Create a new map
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct UploadSubmissionRequest{
|
||||
pub SubmissionID:u64,
|
||||
@@ -73,7 +77,7 @@ pub struct UploadSubmissionRequest{
|
||||
pub ModelName:String,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct UploadMapfixRequest{
|
||||
pub MapfixID:u64,
|
||||
@@ -83,7 +87,7 @@ pub struct UploadMapfixRequest{
|
||||
}
|
||||
|
||||
// Release a new map
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ReleaseSubmissionRequest{
|
||||
pub SubmissionID:u64,
|
||||
@@ -97,14 +101,14 @@ pub struct ReleaseSubmissionRequest{
|
||||
pub Submitter:u64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ReleaseSubmissionsBatchRequest{
|
||||
pub Submissions:Vec<ReleaseSubmissionRequest>,
|
||||
pub OperationID:u32,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ReleaseMapfixRequest{
|
||||
pub MapfixID:u64,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum ReadDomError{
|
||||
Binary(rbx_binary::DecodeError),
|
||||
@@ -31,6 +31,9 @@ fn find_first_child_name_and_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&r
|
||||
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.name==name&&inst.class==class)
|
||||
}
|
||||
|
||||
#[derive(serde_repr::Deserialize_repr)]
|
||||
#[repr(u32)]
|
||||
#[derive(Clone,Copy,PartialEq)]
|
||||
pub enum GameID{
|
||||
Bhop=1,
|
||||
Surf=2,
|
||||
@@ -66,6 +69,15 @@ impl TryFrom<u32> for GameID{
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for GameID{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
match self{
|
||||
GameID::Bhop=>write!(f,"Bhop"),
|
||||
GameID::Surf=>write!(f,"Surf"),
|
||||
GameID::FlyTrials=>write!(f,"FlyTrials"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MapInfo<'a>{
|
||||
pub display_name:Result<&'a str,StringValueError>,
|
||||
@@ -79,6 +91,15 @@ pub enum StringValueError{
|
||||
ValueNotSet,
|
||||
NonStringValue,
|
||||
}
|
||||
impl std::fmt::Display for StringValueError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
match self{
|
||||
StringValueError::ObjectNotFound=>write!(f,"Missing StringValue"),
|
||||
StringValueError::ValueNotSet=>write!(f,"Value not set"),
|
||||
StringValueError::NonStringValue=>write!(f,"Value is not a String"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn string_value(instance:Option<&rbx_dom_weak::Instance>)->Result<&str,StringValueError>{
|
||||
let instance=instance.ok_or(StringValueError::ObjectNotFound)?;
|
||||
|
||||
@@ -183,7 +183,7 @@ async fn release_inner(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
UpdateOperation(tonic::Status),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::download::download_asset_version;
|
||||
use crate::nats_types::UploadMapfixRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum InnerError{
|
||||
Download(crate::download::Error),
|
||||
@@ -43,7 +43,7 @@ async fn upload_inner(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ApiActionMapfixUploaded(tonic::Status),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::download::download_asset_version;
|
||||
use crate::nats_types::UploadSubmissionRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum InnerError{
|
||||
Download(crate::download::Error),
|
||||
@@ -44,7 +44,7 @@ async fn upload_inner(
|
||||
Ok(upload_response.AssetId)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ApiActionSubmissionUploaded(tonic::Status),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::nats_types::ValidateMapfixRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ApiActionMapfixValidate(tonic::Status),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::nats_types::ValidateSubmissionRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ApiActionSubmissionValidate(tonic::Status),
|
||||
|
||||
@@ -17,7 +17,7 @@ fn hash_source(source:&str)->u64{
|
||||
std::hash::Hasher::finish(&hasher)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ModelInfoDownload(rbx_asset::cloud::GetError),
|
||||
@@ -52,7 +52,7 @@ impl std::fmt::Display for Error{
|
||||
}
|
||||
impl std::error::Error for Error{}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct ValidateRequest{
|
||||
pub ModelID:u64,
|
||||
pub ModelVersion:u64,
|
||||
|
||||
34
web/.gitignore
vendored
34
web/.gitignore
vendored
@@ -1,24 +1,12 @@
|
||||
bun.lockb
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -29,12 +17,22 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
# env files
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
@@ -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
|
||||
|
||||
COPY package.json bun.lockb* ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
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
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN bun install
|
||||
RUN bun run build
|
||||
ENTRYPOINT ["bun", "run", "start"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
677
web/bun.lock
677
web/bun.lock
File diff suppressed because it is too large
Load Diff
13
web/index.html
Normal file
13
web/index.html
Normal 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>
|
||||
@@ -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
4142
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,21 +2,24 @@
|
||||
"name": "map-service-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000 --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3000",
|
||||
"lint": "next lint"
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --ext ts,tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^7.3.6",
|
||||
"@mui/material": "^7.3.6",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"date-fns": "^4.1.0",
|
||||
"next": "^16.0.7",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"sass": "^1.94.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -24,8 +27,9 @@
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "16.0.7",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
46
web/src/App.tsx
Normal file
46
web/src/App.tsx
Normal 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
|
||||
@@ -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 Link from "next/link";
|
||||
import { Link } from "react-router-dom";
|
||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||
import {SubmissionInfo} from "@/app/ts/Submission";
|
||||
@@ -65,14 +65,22 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
|
||||
return (
|
||||
<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">
|
||||
{title}
|
||||
</Typography>
|
||||
<Link href={viewAllLink} style={{textDecoration: 'none'}}>
|
||||
<Typography component="span" color="primary">
|
||||
View All →
|
||||
</Typography>
|
||||
<Link to={viewAllLink} style={{textDecoration: 'none'}}>
|
||||
<Button
|
||||
endIcon={<ArrowForwardIosIcon sx={{ fontSize: '0.875rem' }} />}
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
@@ -85,9 +93,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 2,
|
||||
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': {
|
||||
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',
|
||||
}}
|
||||
@@ -106,7 +117,7 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
gap: '16px', // Fixed 16px gap - using string with px unit to ensure it's absolute
|
||||
gap: '20px',
|
||||
padding: '8px 4px',
|
||||
}}
|
||||
>
|
||||
@@ -116,7 +127,7 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
sx={{
|
||||
flex: '0 0 auto',
|
||||
width: {
|
||||
xs: '260px', // Fixed width at different breakpoints
|
||||
xs: '260px',
|
||||
sm: '280px',
|
||||
md: '300px'
|
||||
}
|
||||
@@ -135,9 +146,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 2,
|
||||
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': {
|
||||
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',
|
||||
}}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Avatar,
|
||||
Typography,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
Skeleton
|
||||
} from "@mui/material";
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import { formatDistanceToNow, format } from "date-fns";
|
||||
import { AuditEvent, decodeAuditEvent as auditEventMessage } from "@/app/ts/AuditEvent";
|
||||
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||
|
||||
interface AuditEventItemProps {
|
||||
event: AuditEvent;
|
||||
@@ -15,17 +16,44 @@ interface AuditEventItemProps {
|
||||
}
|
||||
|
||||
export default function AuditEventItem({ event, validatorUser }: AuditEventItemProps) {
|
||||
const isValidator = event.User === validatorUser;
|
||||
const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150');
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Avatar
|
||||
src={event.User === validatorUser ? undefined : `/thumbnails/user/${event.User}`}
|
||||
>
|
||||
<PersonIcon />
|
||||
</Avatar>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
p: 2,
|
||||
borderRadius: 1
|
||||
}}>
|
||||
<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={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="subtitle2">
|
||||
{event.User === validatorUser ? "Validator" : event.Username || "Unknown"}
|
||||
{isValidator ? "Validator" : event.Username || "Unknown"}
|
||||
</Typography>
|
||||
<DateDisplay date={event.Date} />
|
||||
</Box>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Stack,
|
||||
@@ -22,18 +21,21 @@ export default function AuditEventsTabPanel({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box role="tabpanel" hidden={activeTab !== 1}>
|
||||
{activeTab === 1 && (
|
||||
<Stack spacing={2}>
|
||||
{filteredEvents.map((event, index) => (
|
||||
<AuditEventItem
|
||||
key={index}
|
||||
event={event}
|
||||
validatorUser={validatorUser}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
<Box
|
||||
role="tabpanel"
|
||||
sx={{
|
||||
display: activeTab === 1 ? 'block' : 'none'
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
{filteredEvents.map((event, index) => (
|
||||
<AuditEventItem
|
||||
key={index}
|
||||
event={event}
|
||||
validatorUser={validatorUser}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Avatar,
|
||||
Typography,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
Skeleton
|
||||
} from "@mui/material";
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import { formatDistanceToNow, format } from "date-fns";
|
||||
import { AuditEvent, decodeAuditEvent } from "@/app/ts/AuditEvent";
|
||||
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||
|
||||
interface CommentItemProps {
|
||||
event: AuditEvent;
|
||||
@@ -15,21 +16,43 @@ interface CommentItemProps {
|
||||
}
|
||||
|
||||
export default function CommentItem({ event, validatorUser }: CommentItemProps) {
|
||||
const isValidator = event.User === validatorUser;
|
||||
const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150');
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Avatar
|
||||
src={event.User === validatorUser ? undefined : `/thumbnails/user/${event.User}`}
|
||||
>
|
||||
<PersonIcon />
|
||||
</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={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="subtitle2">
|
||||
{event.User === validatorUser ? "Validator" : event.Username || "Unknown"}
|
||||
{isValidator ? "Validator" : event.Username || "Unknown"}
|
||||
</Typography>
|
||||
<DateDisplay date={event.Date} />
|
||||
</Box>
|
||||
<Typography variant="body2">{decodeAuditEvent(event)}</Typography>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>{decodeAuditEvent(event)}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -4,10 +4,22 @@ import {
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
keyframes
|
||||
} from "@mui/material";
|
||||
import CommentsTabPanel from './CommentsTabPanel';
|
||||
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 {
|
||||
auditEvents: AuditEvent[];
|
||||
@@ -16,6 +28,7 @@ interface CommentsAndAuditSectionProps {
|
||||
handleCommentSubmit: () => void;
|
||||
validatorUser: number;
|
||||
userId: number | null;
|
||||
currentStatus?: number;
|
||||
}
|
||||
|
||||
export default function CommentsAndAuditSection({
|
||||
@@ -25,13 +38,24 @@ export default function CommentsAndAuditSection({
|
||||
handleCommentSubmit,
|
||||
validatorUser,
|
||||
userId,
|
||||
currentStatus,
|
||||
}: CommentsAndAuditSectionProps) {
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
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 (
|
||||
<Paper sx={{ p: 3, mt: 3 }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
@@ -41,7 +65,24 @@ export default function CommentsAndAuditSection({
|
||||
aria-label="comments and audit tabs"
|
||||
>
|
||||
<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>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Stack,
|
||||
Avatar,
|
||||
TextField,
|
||||
IconButton
|
||||
IconButton,
|
||||
Skeleton
|
||||
} from "@mui/material";
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
|
||||
import CommentItem from './CommentItem';
|
||||
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||
|
||||
interface CommentsTabPanelProps {
|
||||
activeTab: number;
|
||||
@@ -34,34 +35,35 @@ export default function CommentsTabPanel({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box role="tabpanel" hidden={activeTab !== 0}>
|
||||
{activeTab === 0 && (
|
||||
<>
|
||||
<Stack spacing={2} sx={{ mb: 3 }}>
|
||||
{commentEvents.length > 0 ? (
|
||||
commentEvents.map((event, index) => (
|
||||
<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
|
||||
role="tabpanel"
|
||||
sx={{
|
||||
display: activeTab === 0 ? 'block' : 'none'
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2} sx={{ mb: 3 }}>
|
||||
{commentEvents.length > 0 ? (
|
||||
commentEvents.map((event, index) => (
|
||||
<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>
|
||||
);
|
||||
@@ -75,11 +77,32 @@ interface CommentInputProps {
|
||||
}
|
||||
|
||||
function CommentInput({ newComment, setNewComment, handleCommentSubmit, userId }: CommentInputProps) {
|
||||
const { thumbnailUrl, isLoading } = useUserThumbnail(userId || undefined, '150x150');
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
|
||||
<Avatar
|
||||
src={`/thumbnails/user/${userId}`}
|
||||
/>
|
||||
<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={thumbnailUrl || undefined}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
opacity: isLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import Image from "next/image";
|
||||
import { UserInfo } from "@/app/ts/User";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom"
|
||||
import { useState, useRef } from "react";
|
||||
import { useUser } from "@/app/hooks/useUser";
|
||||
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
@@ -37,7 +34,7 @@ const navItems: HeaderButton[] = [
|
||||
|
||||
function HeaderButton(header: HeaderButton) {
|
||||
return (
|
||||
<Button color="inherit" component={Link} href={header.href}>
|
||||
<Button color="inherit" component={Link} to={header.href}>
|
||||
{header.name}
|
||||
</Button>
|
||||
);
|
||||
@@ -47,14 +44,26 @@ export default function Header() {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const hasAnimated = useRef(false);
|
||||
|
||||
const handleLoginClick = () => {
|
||||
window.location.href =
|
||||
"/auth/oauth2/login?redirect=" + window.location.href;
|
||||
const getAuthUrl = () => {
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
// 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 [user, setUser] = useState<UserInfo | null>(null);
|
||||
const handleLoginClick = () => {
|
||||
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 [quickLinksAnchor, setQuickLinksAnchor] = useState<null | HTMLElement>(null);
|
||||
|
||||
@@ -77,60 +86,34 @@ export default function Header() {
|
||||
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
|
||||
const drawer = (
|
||||
<Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}>
|
||||
<List>
|
||||
{navItems.map((item) => (
|
||||
<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} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
{valid && user && (
|
||||
{isLoggedIn && user && (
|
||||
<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' }} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)}
|
||||
{!valid && (
|
||||
{!isLoggedIn && (
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={handleLoginClick} sx={{ textAlign: 'center' }}>
|
||||
<ListItemText primary="Login" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)}
|
||||
{valid && user && (
|
||||
{isLoggedIn && user && (
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} href="/auth" sx={{ textAlign: 'center' }}>
|
||||
<ListItemButton component="a" href={getAuthUrl()} sx={{ textAlign: 'center' }}>
|
||||
<ListItemText primary="Manage Account" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
@@ -150,7 +133,7 @@ export default function Header() {
|
||||
|
||||
return (
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Toolbar sx={{ py: 1 }}>
|
||||
{isMobile && (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
@@ -165,20 +148,144 @@ export default function Header() {
|
||||
|
||||
{/* Desktop navigation */}
|
||||
{!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) => (
|
||||
<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 */}
|
||||
<Box>
|
||||
<Button
|
||||
color="inherit"
|
||||
endIcon={<ArrowDropDownIcon />}
|
||||
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>
|
||||
<Menu
|
||||
anchorEl={quickLinksAnchor}
|
||||
@@ -186,12 +293,20 @@ export default function Header() {
|
||||
onClose={handleQuickLinksClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
sx={{
|
||||
'& .MuiMenu-paper': {
|
||||
mt: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{quickLinks.map(link => (
|
||||
<MenuItem
|
||||
key={link.name}
|
||||
onClick={handleQuickLinksClose}
|
||||
sx={{ minWidth: 180 }}
|
||||
sx={{
|
||||
minWidth: 200,
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
component="a"
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
@@ -209,30 +324,53 @@ export default function Header() {
|
||||
{isMobile && <Box sx={{ flexGrow: 1 }} />}
|
||||
|
||||
{/* Right side of nav */}
|
||||
<Box display="flex" gap={2}>
|
||||
{!isMobile && valid && user && (
|
||||
<Button variant="outlined" color="success" component={Link} href="/submit">
|
||||
<Box display="flex" gap={2} alignItems="center">
|
||||
{!isMobile && isLoggedIn && user && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
component={Link}
|
||||
to="/submit"
|
||||
sx={{
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
Submit Map
|
||||
</Button>
|
||||
)}
|
||||
{!isMobile && valid && user ? (
|
||||
{!isMobile && isLoggedIn && user ? (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Button
|
||||
onClick={handleMenuOpen}
|
||||
color="inherit"
|
||||
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"
|
||||
width={28}
|
||||
height={28}
|
||||
priority={true}
|
||||
src={user.AvatarURL}
|
||||
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>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
@@ -240,38 +378,58 @@ export default function Header() {
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
horizontal: "right",
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiMenu-paper': {
|
||||
mt: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem component={Link} href="/auth">
|
||||
Manage
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={getAuthUrl()}
|
||||
sx={{
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
>
|
||||
Manage Account
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
) : !isMobile && (
|
||||
<Button color="inherit" onClick={handleLoginClick}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleLoginClick}
|
||||
sx={{
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* In mobile view, display just the avatar if logged in */}
|
||||
{isMobile && valid && user && (
|
||||
{isMobile && isLoggedIn && user && (
|
||||
<IconButton
|
||||
onClick={handleMenuOpen}
|
||||
color="inherit"
|
||||
size="small"
|
||||
>
|
||||
<Image
|
||||
<img
|
||||
className="avatar"
|
||||
width={28}
|
||||
height={28}
|
||||
priority={true}
|
||||
width={32}
|
||||
height={32}
|
||||
src={user.AvatarURL}
|
||||
alt={user.Username}
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
@@ -284,10 +442,13 @@ export default function Header() {
|
||||
open={mobileOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile
|
||||
keepMounted: true,
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: 240 },
|
||||
'& .MuiDrawer-paper': {
|
||||
boxSizing: 'border-box',
|
||||
width: 240,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React from "react";
|
||||
import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Divider, Grid, Typography} from "@mui/material";
|
||||
import {Explore, Person2} from "@mui/icons-material";
|
||||
import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Divider, Typography, Skeleton} from "@mui/material";
|
||||
import {Explore, Person2, Assignment, Build} from "@mui/icons-material";
|
||||
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 {
|
||||
displayName: string;
|
||||
@@ -14,173 +17,176 @@ interface MapCardProps {
|
||||
gameID: number;
|
||||
created: number;
|
||||
type: 'mapfix' | 'submission';
|
||||
showTypeBadge?: boolean;
|
||||
}
|
||||
|
||||
const CARD_WIDTH = 270;
|
||||
|
||||
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 (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }} key={props.assetId}>
|
||||
<Box sx={{
|
||||
width: CARD_WIDTH,
|
||||
mx: 'auto', // Center the card in its grid cell
|
||||
}}>
|
||||
<Card sx={{
|
||||
width: CARD_WIDTH,
|
||||
height: 340, // Fixed height for all cards
|
||||
display: 'flex',
|
||||
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}
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardActionArea
|
||||
component={Link}
|
||||
to={`/${props.type === 'submission' ? 'submissions' : 'mapfixes'}/${props.id}`}>
|
||||
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={180}
|
||||
animation="wave"
|
||||
sx={{
|
||||
height: 160, // Fixed height for all images
|
||||
objectFit: 'cover',
|
||||
position: 'absolute',
|
||||
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
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
opacity: assetLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out 0.1s',
|
||||
}}
|
||||
>
|
||||
<StatusChip status={props.statusID}/>
|
||||
</Box>
|
||||
</Box>
|
||||
<CardContent sx={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
p: 2,
|
||||
width: '100%',
|
||||
}}>
|
||||
<CardContent>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
variant="h6"
|
||||
component="div"
|
||||
sx={{
|
||||
mb: 1,
|
||||
mb: 1.5,
|
||||
fontWeight: 600,
|
||||
color: '#fff',
|
||||
lineHeight: '1.3',
|
||||
// Allow text to wrap
|
||||
lineHeight: '1.4',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
minHeight: '2.8em',
|
||||
}}
|
||||
>
|
||||
{props.displayName}
|
||||
</Typography>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
mb: 1.5,
|
||||
gap: 2,
|
||||
mb: 2,
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Explore 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.gameID === 1 ? 'Bhop' : props.gameID === 2 ? 'Surf' : props.gameID === 5 ? 'Fly Trials' : props.gameID === 4 ? 'Deathrun' : 'Unknown'}
|
||||
</Typography>
|
||||
</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 sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Explore sx={{ fontSize: '1rem', color: '#6366f1' }} />
|
||||
<Typography variant="body2" color="text.secondary" fontSize="0.875rem">
|
||||
{getGameName(props.gameID)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Person2 sx={{ fontSize: '1rem', color: '#8b5cf6' }} />
|
||||
{props.type === 'mapfix' && usernameLoading ? (
|
||||
<Skeleton variant="text" width={80} />
|
||||
) : (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
fontSize="0.875rem"
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{props.type === 'mapfix' && username ? `@${username}` : props.author}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<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
|
||||
src={`/thumbnails/user/${props.authorId}`}
|
||||
src={userThumbnail || undefined}
|
||||
alt={props.author}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
width: 28,
|
||||
height: 28,
|
||||
opacity: userLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
ml: 1,
|
||||
color: 'text.secondary',
|
||||
fontWeight: 500,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{/*In the future author should be the username of the submitter not the info from the map*/}
|
||||
{props.author} - {new Date(props.created * 1000).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
{new Date(props.created * 1000).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Box>
|
||||
</Grid>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Button, Stack } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, Typography, Box } from '@mui/material';
|
||||
import {MapfixInfo } from "@/app/ts/Mapfix";
|
||||
import {hasRole, Roles, RolesConstants} from "@/app/ts/Roles";
|
||||
import {SubmissionInfo} from "@/app/ts/Submission";
|
||||
import {Status, StatusMatches} from "@/app/ts/Status";
|
||||
|
||||
interface ReviewAction {
|
||||
name: string,
|
||||
action: string,
|
||||
name: string;
|
||||
action: string;
|
||||
confirmTitle?: string;
|
||||
confirmMessage?: string;
|
||||
requiresConfirmation: boolean;
|
||||
}
|
||||
|
||||
interface ReviewButtonsProps {
|
||||
@@ -19,20 +22,102 @@ interface ReviewButtonsProps {
|
||||
}
|
||||
|
||||
const ReviewActions = {
|
||||
Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction,
|
||||
AdminSubmit: {name:"Admin Submit",action:"trigger-submit"} as ReviewAction,
|
||||
SubmitUnchecked: {name:"Submit Unchecked", action:"trigger-submit-unchecked"} as ReviewAction,
|
||||
ResetSubmitting: {name:"Reset Submitting",action:"reset-submitting"} as ReviewAction,
|
||||
Revoke: {name:"Revoke",action:"revoke"} as ReviewAction,
|
||||
Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction,
|
||||
Reject: {name:"Reject",action:"reject"} as ReviewAction,
|
||||
Validate: {name:"Validate",action:"retry-validate"} as ReviewAction,
|
||||
ResetValidating: {name:"Reset Validating",action:"reset-validating"} as ReviewAction,
|
||||
RequestChanges: {name:"Request Changes",action:"request-changes"} as ReviewAction,
|
||||
Upload: {name:"Upload",action:"trigger-upload"} as ReviewAction,
|
||||
ResetUploading: {name:"Reset Uploading",action:"reset-uploading"} as ReviewAction,
|
||||
Release: {name:"Release",action:"trigger-release"} as ReviewAction,
|
||||
ResetReleasing: {name:"Reset Releasing",action:"reset-releasing"} as ReviewAction,
|
||||
Submit: {
|
||||
name: "Submit for Review",
|
||||
action: "trigger-submit",
|
||||
confirmTitle: "Submit for Review",
|
||||
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.",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
AdminSubmit: {
|
||||
name: "Submit on Behalf of User",
|
||||
action: "trigger-submit",
|
||||
confirmTitle: "Admin Submit",
|
||||
confirmMessage: "This will submit the work as if the original user did it. Continue?",
|
||||
requiresConfirmation: true
|
||||
} 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> = ({
|
||||
@@ -42,16 +127,46 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
||||
roles,
|
||||
type,
|
||||
}) => {
|
||||
const getVisibleButtons = () => {
|
||||
if (!item || userId === null) return [];
|
||||
const [confirmDialog, setConfirmDialog] = useState<{
|
||||
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 = {
|
||||
action: ReviewAction;
|
||||
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 status = item.StatusID;
|
||||
|
||||
@@ -59,133 +174,215 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
||||
const uploadRole = type === "submission" ? RolesConstants.SubmissionUpload : RolesConstants.MapfixUpload;
|
||||
const releaseRole = type === "submission" ? RolesConstants.SubmissionRelease : RolesConstants.MapfixRelease;
|
||||
|
||||
// Submitter actions
|
||||
if (is_submitter) {
|
||||
if (StatusMatches(status, [Status.UnderConstruction, Status.ChangesRequested])) {
|
||||
buttons.push({
|
||||
submitterButtons.push({
|
||||
action: ReviewActions.Submit,
|
||||
color: "primary"
|
||||
color: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (StatusMatches(status, [Status.Submitted, Status.ChangesRequested])) {
|
||||
buttons.push({
|
||||
submitterButtons.push({
|
||||
action: ReviewActions.Revoke,
|
||||
color: "error"
|
||||
color: "warning",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.Submitting) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
action: ReviewActions.ResetSubmitting,
|
||||
color: "warning"
|
||||
color: "error",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons for review role
|
||||
// Reviewer actions
|
||||
if (hasRole(roles, reviewRole)) {
|
||||
if (status === Status.Submitted && !is_submitter) {
|
||||
buttons.push(
|
||||
{
|
||||
action: ReviewActions.Accept,
|
||||
color: "success"
|
||||
},
|
||||
{
|
||||
action: ReviewActions.Reject,
|
||||
color: "error"
|
||||
}
|
||||
);
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.Accept,
|
||||
color: "success"
|
||||
});
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.Reject,
|
||||
color: "error",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.AcceptedUnvalidated) {
|
||||
buttons.push({
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.Validate,
|
||||
color: "info"
|
||||
color: "primary"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.Validating) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
action: ReviewActions.ResetValidating,
|
||||
color: "warning"
|
||||
color: "error",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
|
||||
if (StatusMatches(status, [Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) {
|
||||
buttons.push({
|
||||
if (StatusMatches(status, [Status.Uploaded, Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) {
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.RequestChanges,
|
||||
color: "warning"
|
||||
color: "warning",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.ChangesRequested) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
action: ReviewActions.SubmitUnchecked,
|
||||
color: "warning"
|
||||
color: "warning",
|
||||
variant: "outlined"
|
||||
});
|
||||
// button only exists for submissions
|
||||
// submitter has normal submit button
|
||||
if (type === "submission" && !is_submitter) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
action: ReviewActions.AdminSubmit,
|
||||
color: "primary"
|
||||
color: "info",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons for upload role
|
||||
// Upload role actions
|
||||
if (hasRole(roles, uploadRole)) {
|
||||
if (status === Status.Validated) {
|
||||
buttons.push({
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.Upload,
|
||||
color: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.Uploading) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
action: ReviewActions.ResetUploading,
|
||||
color: "warning"
|
||||
color: "error",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons for release role
|
||||
// Release role actions
|
||||
if (hasRole(roles, releaseRole)) {
|
||||
// submissions do not have a release button
|
||||
if (type === "mapfix" && status === Status.Uploaded) {
|
||||
buttons.push({
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.Release,
|
||||
color: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.Releasing) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
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 (
|
||||
<Stack spacing={2} sx={{ mb: 3 }}>
|
||||
{getVisibleButtons().map((button, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="contained"
|
||||
color={button.color}
|
||||
fullWidth
|
||||
onClick={() => onClick(button.action.action, item.ID)}
|
||||
>
|
||||
{button.action.name}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
<>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<ActionCard title="Your Actions" actions={buttons.submitter} isFirst={true} />
|
||||
<ActionCard title="Review Actions" actions={buttons.reviewer} isFirst={buttons.submitter.length === 0} />
|
||||
<ActionCard title="Admin Actions" actions={buttons.admin} isFirst={buttons.submitter.length === 0 && buttons.reviewer.length === 0} />
|
||||
</Box>
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={confirmDialog.open}
|
||||
onClose={handleCancel}
|
||||
maxWidth="xs"
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 { CopyableField } from "@/app/_components/review/CopyableField";
|
||||
import WorkflowStepper from "./WorkflowStepper";
|
||||
import { SubmissionInfo } from "@/app/ts/Submission";
|
||||
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
|
||||
interface FieldConfig {
|
||||
@@ -16,12 +23,24 @@ type ReviewItemType = SubmissionInfo | MapfixInfo;
|
||||
interface ReviewItemProps {
|
||||
item: ReviewItemType;
|
||||
handleCopyValue: (value: string) => void;
|
||||
currentUserId?: number;
|
||||
userId?: number | null;
|
||||
onDescriptionUpdate?: () => Promise<void>;
|
||||
showSnackbar?: (message: string, severity?: 'success' | 'error' | 'info' | 'warning') => void;
|
||||
}
|
||||
|
||||
export function ReviewItem({
|
||||
item,
|
||||
handleCopyValue
|
||||
handleCopyValue,
|
||||
currentUserId,
|
||||
userId,
|
||||
onDescriptionUpdate,
|
||||
showSnackbar
|
||||
}: ReviewItemProps) {
|
||||
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
||||
const [editedDescription, setEditedDescription] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Type guard to check if item is valid
|
||||
if (!item) return null;
|
||||
|
||||
@@ -29,6 +48,57 @@ export function ReviewItem({
|
||||
const isSubmission = 'UploadedAssetID' 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
|
||||
let fields: FieldConfig[] = [];
|
||||
if (isSubmission) {
|
||||
@@ -46,17 +116,18 @@ export function ReviewItem({
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4 }}>
|
||||
<ReviewItemHeader
|
||||
displayName={item.DisplayName}
|
||||
assetId={isMapfix ? item.TargetAssetID : undefined}
|
||||
statusId={item.StatusID}
|
||||
creator={item.Creator}
|
||||
submitterId={item.Submitter}
|
||||
/>
|
||||
<>
|
||||
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4 }}>
|
||||
<ReviewItemHeader
|
||||
displayName={item.DisplayName}
|
||||
assetId={isMapfix ? item.TargetAssetID : undefined}
|
||||
statusId={item.StatusID}
|
||||
creator={item.Creator}
|
||||
submitterId={item.Submitter}
|
||||
/>
|
||||
|
||||
{/* Item Details */}
|
||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||
{/* Item Details */}
|
||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||
{fields.map((field) => {
|
||||
const fieldValue = (item as never)[field.key];
|
||||
const displayValue = fieldValue === 0 || fieldValue == null ? 'N/A' : fieldValue;
|
||||
@@ -74,19 +145,83 @@ export function ReviewItem({
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
{/* Description Section */}
|
||||
{isMapfix && item.Description && (
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
Description
|
||||
<Grid size={{ xs: 12, sm: 6}}>
|
||||
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||
Game
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{item.Description}
|
||||
{getGameName(item.GameID)}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 { SubmissionStatus } from "@/app/ts/Submission";
|
||||
import { MapfixStatus } from "@/app/ts/Mapfix";
|
||||
import {Status, StatusMatches} from "@/app/ts/Status";
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { Link } from "react-router-dom";
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||
import { useUsername } from "@/app/hooks/useUsername";
|
||||
|
||||
function SubmitterName({ submitterId }: { submitterId: number }) {
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { username, isLoading } = useUsername(submitterId);
|
||||
|
||||
useEffect(() => {
|
||||
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]);
|
||||
const displayName = username ? `@${username}` : String(submitterId);
|
||||
|
||||
if (loading) return <Typography variant="body1">Loading...</Typography>;
|
||||
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' } }}>
|
||||
<Typography>
|
||||
{name || submitterId}
|
||||
</Typography>
|
||||
<LaunchIcon sx={{ fontSize: '1rem', color: 'text.secondary' }} />
|
||||
return <a 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' }}>
|
||||
<Skeleton
|
||||
variant="text"
|
||||
width={80}
|
||||
sx={{
|
||||
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>
|
||||
</Link>
|
||||
</a>
|
||||
}
|
||||
|
||||
interface ReviewItemHeaderProps {
|
||||
@@ -49,7 +50,8 @@ interface 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`
|
||||
0%, 100% { opacity: 0.2; transform: scale(0.8); }
|
||||
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 }}>
|
||||
{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">
|
||||
<Typography
|
||||
variant="h4"
|
||||
@@ -111,10 +113,28 @@ export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, subm
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<Avatar
|
||||
src={`/thumbnails/user/${submitterId}`}
|
||||
sx={{ mr: 1, width: 24, height: 24 }}
|
||||
/>
|
||||
<Box sx={{ position: 'relative', mr: 1, width: 24, height: 24 }}>
|
||||
<Skeleton
|
||||
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} />
|
||||
</Box>
|
||||
</>
|
||||
|
||||
315
web/src/app/_components/review/WorkflowStepper.tsx
Normal file
315
web/src/app/_components/review/WorkflowStepper.tsx
Normal 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;
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, {JSX} from "react";
|
||||
import {JSX} from "react";
|
||||
import {Cancel, CheckCircle, Pending} from "@mui/icons-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 icon: JSX.Element = <Pending fontSize="small"/>;
|
||||
let label: string = 'Unknown';
|
||||
@@ -81,12 +81,6 @@ export const StatusChip = ({status}: { status: number }) => {
|
||||
label={label}
|
||||
color={color}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 24,
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import Header from "./header";
|
||||
|
||||
export default function Webpage({children}: Readonly<{children?: React.ReactNode}>) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../../globals.scss";
|
||||
@use "../globals.scss";
|
||||
|
||||
::placeholder {
|
||||
color: var(--placeholder-text)
|
||||
@@ -47,8 +47,4 @@ header h1 {
|
||||
form {
|
||||
display: grid;
|
||||
gap: 25px;
|
||||
|
||||
fieldset {
|
||||
border: blue
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import { Button, TextField } from "@mui/material"
|
||||
|
||||
import GameSelection from "./_game";
|
||||
@@ -8,7 +6,7 @@ import Webpage from "@/app/_components/webpage"
|
||||
import React, { useState } from "react";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
import "./(styles)/page.scss"
|
||||
import "./page.scss"
|
||||
|
||||
interface SubmissionPayload {
|
||||
AssetID: number;
|
||||
@@ -43,7 +41,7 @@ export default function SubmissionInfoPage() {
|
||||
|
||||
try {
|
||||
// Send the POST request
|
||||
const response = await fetch("/api/submissions-admin", {
|
||||
const response = await fetch("/v1/submissions-admin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
|
||||
@@ -8,39 +8,23 @@ $form-label-fontsize: 1.3rem;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
color-scheme: dark;
|
||||
|
||||
--header-height: 45px;
|
||||
|
||||
--page: white;
|
||||
--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: #c8c8c8;
|
||||
--text-color: #1e1e1e;
|
||||
--review-border: rgb(50,50,50);
|
||||
--text-color: rgb(230,230,230);
|
||||
--anchor-link-review: #008fd6;
|
||||
--window-header: #f5f5f5;
|
||||
--window-header: rgb(10,10,10);
|
||||
--comment-highlighted: #ffffd7;
|
||||
--comment-area: white;
|
||||
--placeholder-text: rgb(150,150,150);
|
||||
|
||||
@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);
|
||||
}
|
||||
--comment-area: rgb(20,20,20);
|
||||
--placeholder-text: rgb(80,80,80);
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -40,11 +40,11 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
||||
|
||||
try {
|
||||
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}`);
|
||||
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}`);
|
||||
return res.json();
|
||||
})
|
||||
@@ -58,7 +58,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
||||
}
|
||||
|
||||
try {
|
||||
const rolesResponse = await fetch("/api/session/roles");
|
||||
const rolesResponse = await fetch("/v1/session/roles");
|
||||
if (rolesResponse.ok) {
|
||||
const rolesData = await rolesResponse.json();
|
||||
setRoles(rolesData.Roles);
|
||||
@@ -72,7 +72,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
||||
}
|
||||
|
||||
try {
|
||||
const userResponse = await fetch("/api/session/user");
|
||||
const userResponse = await fetch("/v1/session/user");
|
||||
if (userResponse.ok) {
|
||||
const userData = await userResponse.json();
|
||||
setUser(userData.UserID);
|
||||
@@ -100,7 +100,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
||||
|
||||
useEffect(() => {
|
||||
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(() => {
|
||||
fetchData(true);
|
||||
}, 5000);
|
||||
|
||||
216
web/src/app/hooks/useThumbnails.ts
Normal file
216
web/src/app/hooks/useThumbnails.ts
Normal 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]);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useTitle(title: string) {
|
||||
|
||||
39
web/src/app/hooks/useUser.ts
Normal file
39
web/src/app/hooks/useUser.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
103
web/src/app/hooks/useUsername.ts
Normal file
103
web/src/app/hooks/useUsername.ts
Normal 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 };
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,116 @@
|
||||
import {createTheme} from "@mui/material";
|
||||
|
||||
export const theme = createTheme({
|
||||
cssVariables: {
|
||||
colorSchemeSelector: 'class',
|
||||
},
|
||||
colorSchemes: {
|
||||
dark: true,
|
||||
},
|
||||
defaultColorScheme: 'dark',
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: '#90caf9',
|
||||
main: '#3b82f6',
|
||||
dark: '#2563eb',
|
||||
light: '#60a5fa',
|
||||
},
|
||||
secondary: {
|
||||
main: '#f48fb1',
|
||||
main: '#8b5cf6',
|
||||
dark: '#7c3aed',
|
||||
light: '#a78bfa',
|
||||
},
|
||||
background: {
|
||||
default: '#121212',
|
||||
paper: '#1e1e1e',
|
||||
default: '#0a0a0a',
|
||||
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: {
|
||||
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: {
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.5px',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h6: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
subtitle1: {
|
||||
fontWeight: 500,
|
||||
fontSize: '0.95rem',
|
||||
fontSize: '1rem',
|
||||
},
|
||||
body1: {
|
||||
fontSize: '1rem',
|
||||
lineHeight: 1.7,
|
||||
},
|
||||
body2: {
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: 1.6,
|
||||
},
|
||||
caption: {
|
||||
fontSize: '0.75rem',
|
||||
},
|
||||
button: {
|
||||
fontWeight: 600,
|
||||
textTransform: 'none',
|
||||
letterSpacing: '0.01em',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 8,
|
||||
borderRadius: 12,
|
||||
},
|
||||
components: {
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 8,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out',
|
||||
backgroundColor: '#171717',
|
||||
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': {
|
||||
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: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
transition: 'transform 0.3s',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -69,14 +135,48 @@ export const theme = createTheme({
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
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: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
borderColor: 'rgba(148, 163, 184, 0.1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -84,6 +184,126 @@ export const theme = createTheme({
|
||||
styleOverrides: {
|
||||
root: {
|
||||
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)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const thumbnailLoader = ({ src, width, quality }: { src: string, width: number, quality?: number }) => {
|
||||
return `${src}?w=${width}&q=${quality || 75}`;
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import Webpage from "@/app/_components/webpage";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {useState, useEffect} from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAssetThumbnail } from "@/app/hooks/useThumbnails";
|
||||
|
||||
// MUI Components
|
||||
import {
|
||||
@@ -36,7 +35,7 @@ interface SnackbarState {
|
||||
|
||||
export default function MapfixDetailsPage() {
|
||||
const { mapfixId } = useParams<{ mapfixId: string }>();
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const [newComment, setNewComment] = useState("");
|
||||
const [showBeforeImage, setShowBeforeImage] = useState(false);
|
||||
const [snackbar, setSnackbar] = useState<SnackbarState>({
|
||||
@@ -70,16 +69,26 @@ export default function MapfixDetailsPage() {
|
||||
refreshData
|
||||
} = useReviewData({
|
||||
itemType: 'mapfixes',
|
||||
itemId: mapfixId
|
||||
itemId: mapfixId!
|
||||
});
|
||||
const mapfix = mapfixData as MapfixInfo;
|
||||
|
||||
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
|
||||
async function handleReviewAction(action: string, mapfixId: number) {
|
||||
try {
|
||||
const response = await fetch(`/api/mapfixes/${mapfixId}/status/${action}`, {
|
||||
const response = await fetch(`/v1/mapfixes/${mapfixId}/status/${action}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"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 () => {
|
||||
if (!newComment.trim()) {
|
||||
return; // Don't submit empty comments
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/mapfixes/${mapfixId}/comment`, {
|
||||
const response = await fetch(`/v1/mapfixes/${mapfixId}/comment`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
@@ -177,7 +195,7 @@ export default function MapfixDetailsPage() {
|
||||
title="Error Loading Mapfix"
|
||||
message={error || "Mapfix not found"}
|
||||
buttonText="Return to Mapfixes"
|
||||
onButtonClick={() => router.push('/mapfixes')}
|
||||
onButtonClick={() => navigate('/mapfixes')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -191,10 +209,10 @@ export default function MapfixDetailsPage() {
|
||||
aria-label="breadcrumb"
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography color="text.primary">Home</Typography>
|
||||
</Link>
|
||||
<Link href="/mapfixes" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Link to="/mapfixes" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography color="text.primary">Mapfixes</Typography>
|
||||
</Link>
|
||||
<Typography color="text.secondary">{mapfix.DisplayName}</Typography>
|
||||
@@ -207,6 +225,22 @@ export default function MapfixDetailsPage() {
|
||||
<Box sx={{ position: 'relative', width: '100%', aspectRatio: '1/1' }}>
|
||||
{/* Before/After Images Container */}
|
||||
<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 */}
|
||||
<Box
|
||||
sx={{
|
||||
@@ -216,18 +250,34 @@ export default function MapfixDetailsPage() {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 1,
|
||||
opacity: showBeforeImage ? 1 : 0,
|
||||
opacity: showBeforeImage ? (beforeLoading ? 0 : 1) : 0,
|
||||
transition: 'opacity 0.5s ease-in-out'
|
||||
}}
|
||||
>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={`/thumbnails/asset/${mapfix.TargetAssetID}`}
|
||||
image={beforeThumbnail || '/placeholder-map.png'}
|
||||
alt="Before Map Thumbnail"
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
</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 */}
|
||||
<Box
|
||||
sx={{
|
||||
@@ -237,13 +287,13 @@ export default function MapfixDetailsPage() {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 0,
|
||||
opacity: showBeforeImage ? 0 : 1,
|
||||
opacity: showBeforeImage ? 0 : (afterLoading ? 0 : 1),
|
||||
transition: 'opacity 0.5s ease-in-out'
|
||||
}}
|
||||
>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={`/thumbnails/asset/${mapfix.AssetID}`}
|
||||
image={afterThumbnail || '/placeholder-map.png'}
|
||||
alt="After Map Thumbnail"
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
@@ -282,33 +332,6 @@ export default function MapfixDetailsPage() {
|
||||
)}
|
||||
</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
|
||||
sx={{
|
||||
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))',
|
||||
},
|
||||
}}
|
||||
onClick={() => setShowBeforeImage(!showBeforeImage)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -343,6 +365,10 @@ export default function MapfixDetailsPage() {
|
||||
<ReviewItem
|
||||
item={mapfix}
|
||||
handleCopyValue={handleCopyId}
|
||||
currentUserId={user ?? undefined}
|
||||
userId={user}
|
||||
onDescriptionUpdate={() => refreshData(true)}
|
||||
showSnackbar={showSnackbar}
|
||||
/>
|
||||
|
||||
{/* Comments Section */}
|
||||
@@ -353,6 +379,7 @@ export default function MapfixDetailsPage() {
|
||||
handleCommentSubmit={handleCommentSubmit}
|
||||
validatorUser={validatorUser}
|
||||
userId={user}
|
||||
currentStatus={mapfix.StatusID}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { MapfixList } from "../ts/Mapfix";
|
||||
import { MapCard } from "../_components/mapCard";
|
||||
@@ -8,12 +6,14 @@ import { ListSortConstants } from "../ts/Sort";
|
||||
import {
|
||||
Box,
|
||||
Breadcrumbs,
|
||||
CircularProgress,
|
||||
Card,
|
||||
CardContent,
|
||||
Container,
|
||||
Pagination,
|
||||
Skeleton,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import { Link } from "react-router-dom";
|
||||
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function MapfixInfoPage() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
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 }
|
||||
);
|
||||
|
||||
@@ -55,33 +55,38 @@ export default function MapfixInfoPage() {
|
||||
return () => controller.abort();
|
||||
}, [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 (
|
||||
<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 mapfixes...
|
||||
</Typography>
|
||||
</Box>
|
||||
<Container sx={{ py: 6 }}>
|
||||
<Typography variant="body1">
|
||||
Mapfixes list is empty.
|
||||
</Typography>
|
||||
</Container>
|
||||
</Webpage>
|
||||
);
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(mapfixes.Total / cardsPerPage);
|
||||
|
||||
return (
|
||||
<Webpage>
|
||||
<Container maxWidth="lg" sx={{ py: 6 }}>
|
||||
<Box component="main" sx={{ width: '100%', px: 2 }}>
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
py: 6,
|
||||
px: 2,
|
||||
boxSizing: 'border-box'
|
||||
}}>
|
||||
<Box sx={{ width: '100%', maxWidth: '1200px', minWidth: 0 }}>
|
||||
<Breadcrumbs
|
||||
separator={<NavigateNextIcon fontSize="small" />}
|
||||
aria-label="breadcrumb"
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography color="text.primary">Home</Typography>
|
||||
</Link>
|
||||
<Typography color="text.secondary">Mapfixes</Typography>
|
||||
@@ -99,26 +104,52 @@ export default function MapfixInfoPage() {
|
||||
className="grid"
|
||||
sx={{
|
||||
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,
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{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"
|
||||
/>
|
||||
))}
|
||||
{!mapfixes || isLoading ? (
|
||||
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>
|
||||
))
|
||||
) : (
|
||||
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>
|
||||
|
||||
{totalPages > 1 && (
|
||||
@@ -133,7 +164,7 @@ export default function MapfixInfoPage() {
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</Webpage>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Button,
|
||||
@@ -16,10 +14,10 @@ import {
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
||||
import Webpage from "@/app/_components/webpage";
|
||||
import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||
import {MapInfo} from "@/app/ts/Map";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
import { getGameName } from "@/app/utils/games";
|
||||
|
||||
interface MapfixPayload {
|
||||
AssetID: number;
|
||||
@@ -27,15 +25,9 @@ interface MapfixPayload {
|
||||
Description: string;
|
||||
}
|
||||
|
||||
// Game ID mapping
|
||||
const gameTypes: Record<number, string> = {
|
||||
1: "Bhop",
|
||||
2: "Surf",
|
||||
5: "Flytrials"
|
||||
};
|
||||
|
||||
export default function MapfixInfoPage() {
|
||||
const { mapId } = useParams<{ mapId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [mapDetails, setMapDetails] = useState<MapInfo | null>(null);
|
||||
@@ -48,7 +40,7 @@ export default function MapfixInfoPage() {
|
||||
const fetchMapDetails = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch(`/api/maps/${mapId}`);
|
||||
const response = await fetch(`/v1/maps/${mapId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch map details: ${response.statusText}`);
|
||||
@@ -69,12 +61,6 @@ export default function MapfixInfoPage() {
|
||||
}
|
||||
}, [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>) => {
|
||||
event.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
@@ -106,7 +92,7 @@ export default function MapfixInfoPage() {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/mapfixes", {
|
||||
const response = await fetch("/v1/mapfixes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
@@ -118,7 +104,7 @@ export default function MapfixInfoPage() {
|
||||
}
|
||||
|
||||
const { OperationID } = await response.json();
|
||||
window.location.assign(`/operations/${OperationID}`);
|
||||
navigate(`/operations/${OperationID}`);
|
||||
} catch (error) {
|
||||
console.error("Error submitting data:", error);
|
||||
setError(error instanceof Error ? error.message : "An unknown error occurred");
|
||||
@@ -134,14 +120,14 @@ export default function MapfixInfoPage() {
|
||||
aria-label="breadcrumb"
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography color="text.primary">Home</Typography>
|
||||
</Link>
|
||||
<Link href="/maps" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Link to="/maps" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography color="text.primary">Maps</Typography>
|
||||
</Link>
|
||||
{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>
|
||||
</Link>
|
||||
)}
|
||||
@@ -201,7 +187,7 @@ export default function MapfixInfoPage() {
|
||||
label="Game Type"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={getGameType(mapDetails?.GameID)}
|
||||
value={mapDetails?.GameID ? getGameName(mapDetails.GameID) : "Unknown"}
|
||||
disabled
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { MapInfo } from "@/app/ts/Map";
|
||||
import Webpage from "@/app/_components/webpage";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
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 { useAssetThumbnail } from "@/app/hooks/useThumbnails";
|
||||
import { getGameInfo } from "@/app/utils/games";
|
||||
|
||||
// MUI Components
|
||||
import {
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
Stack,
|
||||
CardMedia,
|
||||
Tooltip,
|
||||
IconButton
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
Pagination
|
||||
} from "@mui/material";
|
||||
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
||||
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 InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||
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 {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
export default function MapDetails() {
|
||||
const { mapId } = useParams();
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const [map, setMap] = useState<MapInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
const [roles, setRoles] = useState(RolesConstants.Empty);
|
||||
const [mapfixes, setMapfixes] = useState<MapfixInfo[]>([]);
|
||||
const [fixesPage, setFixesPage] = useState(1);
|
||||
|
||||
useTitle(map ? `${map.DisplayName}` : 'Loading Map...');
|
||||
|
||||
// Use thumbnail hook for the map preview image
|
||||
const { thumbnailUrl, isLoading: thumbnailLoading } = useAssetThumbnail(
|
||||
map?.ID,
|
||||
'768x432'
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
async function getMap() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch(`/api/maps/${mapId}`);
|
||||
const res = await fetch(`/v1/maps/${mapId}`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch map: ${res.status}`);
|
||||
}
|
||||
@@ -73,7 +89,7 @@ export default function MapDetails() {
|
||||
useEffect(() => {
|
||||
async function getRoles() {
|
||||
try {
|
||||
const rolesResponse = await fetch("/api/session/roles");
|
||||
const rolesResponse = await fetch("/v1/session/roles");
|
||||
if (rolesResponse.ok) {
|
||||
const rolesData = await rolesResponse.json();
|
||||
setRoles(rolesData.Roles);
|
||||
@@ -99,16 +115,15 @@ export default function MapDetails() {
|
||||
let allMapfixes: MapfixInfo[] = [];
|
||||
let total = 0;
|
||||
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;
|
||||
const data = await res.json();
|
||||
if (page === 1) total = data.Total;
|
||||
allMapfixes = allMapfixes.concat(data.Mapfixes);
|
||||
page++;
|
||||
} while (allMapfixes.length < total);
|
||||
// Filter out rejected, uploading, uploaded (StatusID > 7)
|
||||
const active = allMapfixes.filter((fix: MapfixInfo) => fix.StatusID <= MapfixStatus.Validated);
|
||||
setMapfixes(active);
|
||||
// Store all mapfixes for history display
|
||||
setMapfixes(allMapfixes);
|
||||
} catch {
|
||||
setMapfixes([]);
|
||||
}
|
||||
@@ -124,33 +139,18 @@ export default function MapDetails() {
|
||||
});
|
||||
};
|
||||
|
||||
const getGameInfo = (gameId: number) => {
|
||||
switch (gameId) {
|
||||
case 1:
|
||||
return {
|
||||
name: "Bhop",
|
||||
color: "#2196f3" // blue
|
||||
};
|
||||
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 getStatusIcon = (iconName: string) => {
|
||||
switch (iconName) {
|
||||
case "Build": return BuildIcon;
|
||||
case "Pending": return PendingIcon;
|
||||
case "CheckCircle": return CheckCircleIcon;
|
||||
case "Cancel": return CancelIcon;
|
||||
default: return PendingIcon;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitMapfix = () => {
|
||||
router.push(`/maps/${mapId}/fix`);
|
||||
navigate(`/maps/${mapId}/fix`);
|
||||
};
|
||||
|
||||
const handleCopyId = (idToCopy: string) => {
|
||||
@@ -180,7 +180,7 @@ export default function MapDetails() {
|
||||
<Typography variant="body1">{error}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => router.push('/maps')}
|
||||
onClick={() => navigate('/maps')}
|
||||
sx={{ mt: 3 }}
|
||||
>
|
||||
Return to Maps
|
||||
@@ -200,10 +200,10 @@ export default function MapDetails() {
|
||||
aria-label="breadcrumb"
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography color="text.primary">Home</Typography>
|
||||
</Link>
|
||||
<Link href="/maps" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Link to="/maps" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography color="text.primary">Maps</Typography>
|
||||
</Link>
|
||||
<Typography color="text.secondary">{loading ? "Loading..." : map?.DisplayName || "Map Details"}</Typography>
|
||||
@@ -299,7 +299,7 @@ export default function MapDetails() {
|
||||
<IconButton
|
||||
size="small"
|
||||
component="a"
|
||||
href={`/api/maps/${mapId}/download`}
|
||||
href={`/v1/maps/${mapId}/download`}
|
||||
download={`${map?.DisplayName}.rbxm`}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
@@ -319,19 +319,263 @@ export default function MapDetails() {
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
position: 'relative'
|
||||
position: 'relative',
|
||||
mb: 3
|
||||
}}
|
||||
>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={`/thumbnails/asset/${map.ID}`}
|
||||
alt={`Preview of map: ${map.DisplayName}`}
|
||||
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={400}
|
||||
animation="wave"
|
||||
sx={{
|
||||
height: 400,
|
||||
objectFit: 'cover',
|
||||
position: 'absolute',
|
||||
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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Map Details Section */}
|
||||
@@ -376,39 +620,6 @@ export default function MapDetails() {
|
||||
</Tooltip>
|
||||
</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>
|
||||
</Paper>
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import {useState, useEffect} from "react";
|
||||
import Image from "next/image";
|
||||
import {useRouter} from "next/navigation";
|
||||
import Webpage from "@/app/_components/webpage";
|
||||
import {
|
||||
Box,
|
||||
@@ -16,18 +12,20 @@ import {
|
||||
TextField,
|
||||
InputAdornment,
|
||||
Pagination,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
SelectChangeEvent, Breadcrumbs
|
||||
SelectChangeEvent,
|
||||
Breadcrumbs,
|
||||
Skeleton
|
||||
} from "@mui/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 {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 {
|
||||
ID: number;
|
||||
@@ -37,10 +35,92 @@ interface Map {
|
||||
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() {
|
||||
useTitle("Map Collection");
|
||||
|
||||
const router = useRouter();
|
||||
const [maps, setMaps] = useState<Map[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -59,7 +139,7 @@ export default function MapsPage() {
|
||||
let hasMore = true;
|
||||
|
||||
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();
|
||||
allMaps = [...allMaps, ...data];
|
||||
hasMore = data.length === requestPageSize;
|
||||
@@ -102,15 +182,17 @@ export default function MapsPage() {
|
||||
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) => {
|
||||
setCurrentPage(page);
|
||||
window.scrollTo({top: 0, behavior: 'smooth'});
|
||||
};
|
||||
|
||||
const handleMapClick = (mapId: number) => {
|
||||
router.push(`/maps/${mapId}`);
|
||||
};
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
@@ -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 (
|
||||
<Webpage>
|
||||
<Container maxWidth="lg" sx={{py: 6}}>
|
||||
@@ -166,7 +210,7 @@ export default function MapsPage() {
|
||||
aria-label="breadcrumb"
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography color="text.primary">Home</Typography>
|
||||
</Link>
|
||||
<Typography color="text.secondary">Maps</Typography>
|
||||
@@ -197,9 +241,27 @@ export default function MapsPage() {
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<Box display="flex" justifyContent="center" my={8}>
|
||||
<CircularProgress/>
|
||||
</Box>
|
||||
<Grid container spacing={3}>
|
||||
{Array.from({ length: mapsPerPage }).map((_, index) => (
|
||||
<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}>
|
||||
@@ -227,62 +289,10 @@ export default function MapsPage() {
|
||||
<Grid container spacing={3}>
|
||||
{currentMaps.map((map) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4}} key={map.ID}>
|
||||
<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 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>
|
||||
<MapCard
|
||||
map={map}
|
||||
formatDate={formatDate}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
191
web/src/app/not-found/page.tsx
Normal file
191
web/src/app/not-found/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {useEffect, useState, useRef, ReactElement} from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CircularProgress,
|
||||
Typography,
|
||||
@@ -13,7 +11,11 @@ import {
|
||||
Divider,
|
||||
Alert,
|
||||
Collapse,
|
||||
IconButton
|
||||
IconButton,
|
||||
Fade,
|
||||
Grow,
|
||||
Slide,
|
||||
keyframes
|
||||
} from "@mui/material";
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
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 {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 {
|
||||
OperationID: number;
|
||||
Status: number;
|
||||
@@ -33,7 +96,7 @@ interface Operation {
|
||||
}
|
||||
|
||||
export default function OperationStatusPage() {
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const { operationId } = useParams();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -49,7 +112,7 @@ export default function OperationStatusPage() {
|
||||
|
||||
const fetchOperation = async () => {
|
||||
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");
|
||||
|
||||
@@ -72,13 +135,12 @@ export default function OperationStatusPage() {
|
||||
};
|
||||
|
||||
fetchOperation();
|
||||
if (!intervalRef.current) {
|
||||
intervalRef.current = setInterval(fetchOperation, 1000);
|
||||
}
|
||||
intervalRef.current = setInterval(fetchOperation, 1000);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [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 (
|
||||
<Webpage>
|
||||
<Container maxWidth="md" sx={{ py: 6 }}>
|
||||
<Typography variant="h4" component="h1" fontWeight="bold" mb={4}>
|
||||
Operation Status
|
||||
</Typography>
|
||||
<Fade in timeout={500}>
|
||||
<Typography variant="h4" component="h1" fontWeight="bold" mb={4}>
|
||||
Operation Status
|
||||
</Typography>
|
||||
</Fade>
|
||||
|
||||
{loading ? (
|
||||
<Box display="flex" flexDirection="column" alignItems="center" my={8}>
|
||||
@@ -149,33 +226,47 @@ export default function OperationStatusPage() {
|
||||
</Typography>
|
||||
</Box>
|
||||
) : error ? (
|
||||
<Alert severity="error" sx={{ my: 2 }}>
|
||||
<Typography variant="body1">{error}</Typography>
|
||||
</Alert>
|
||||
<Slide direction="up" in mountOnEnter unmountOnExit>
|
||||
<Alert severity="error" sx={{ my: 2 }}>
|
||||
<Typography variant="body1">{error}</Typography>
|
||||
</Alert>
|
||||
</Slide>
|
||||
) : operation ? (
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: 2,
|
||||
border: 1,
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="h5">
|
||||
Operation #{operation.OperationID}
|
||||
</Typography>
|
||||
<Chip
|
||||
icon={getStatusIcon(operation.Status)}
|
||||
label={getStatusText(operation.Status)}
|
||||
color={getStatusColor(operation.Status) as "success" | "warning" | "error" | "default"}
|
||||
variant="filled"
|
||||
sx={{ fontWeight: 'bold', px: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
<Grow in timeout={600}>
|
||||
<Box>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: 2,
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
animation: `${slideInUp} 0.5s ease-out`
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="h5">
|
||||
Operation #{operation.OperationID}
|
||||
</Typography>
|
||||
<Chip
|
||||
icon={getStatusIcon(operation.Status)}
|
||||
label={getStatusText(operation.Status)}
|
||||
color={getStatusColor(operation.Status) as "success" | "warning" | "error" | "default"}
|
||||
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 }}>
|
||||
<Typography variant="body1" color="text.secondary" gutterBottom>
|
||||
@@ -233,24 +324,35 @@ export default function OperationStatusPage() {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{operation.Status === 1 && (
|
||||
<Box sx={{ mt: 4, textAlign: 'center' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
onClick={() => router.push(operation.Path)}
|
||||
startIcon={<CheckCircleIcon />}
|
||||
>
|
||||
Next Step
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
{operation.Status === 1 && (
|
||||
<Box sx={{ mt: 4, textAlign: 'center' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
onClick={() => navigate(operation.Path)}
|
||||
startIcon={<CheckCircleIcon />}
|
||||
sx={{
|
||||
animation: `${successPop} 0.6s ease-out`,
|
||||
transition: 'transform 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Next Step
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Grow>
|
||||
) : (
|
||||
<Alert severity="info" sx={{ my: 2 }}>
|
||||
<Typography variant="body1">No operation found with ID: {operationId}</Typography>
|
||||
</Alert>
|
||||
<Fade in>
|
||||
<Alert severity="info" sx={{ my: 2 }}>
|
||||
<Typography variant="body1">No operation found with ID: {operationId}</Typography>
|
||||
</Alert>
|
||||
</Fade>
|
||||
)}
|
||||
</Container>
|
||||
</Webpage>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user