Compare commits
6 Commits
master
...
f68a9492a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
f68a9492a8
|
|||
|
1dbf4fc745
|
|||
|
a425524fdd
|
|||
|
57654ad9a6
|
|||
|
3927c525dd
|
|||
|
46a4e5a8ca
|
39
Cargo.lock
generated
39
Cargo.lock
generated
@@ -1484,6 +1484,21 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
@@ -1491,6 +1506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1499,6 +1515,23 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
@@ -1528,10 +1561,13 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
@@ -2356,7 +2392,6 @@ dependencies = [
|
||||
"async-nats",
|
||||
"aws-config",
|
||||
"aws-sdk-s3",
|
||||
"futures-util",
|
||||
"map-tool",
|
||||
"rbx_asset",
|
||||
"rbx_binary",
|
||||
@@ -2376,7 +2411,7 @@ name = "maps-validation"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"async-nats",
|
||||
"futures-util",
|
||||
"futures",
|
||||
"heck",
|
||||
"rbx_asset",
|
||||
"rbx_binary",
|
||||
|
||||
@@ -8,7 +8,6 @@ resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
async-nats = "0.46.0"
|
||||
futures-util = "0.3.31"
|
||||
rbx_asset = { version = "0.5.0", features = ["gzip", "rustls-tls"], default-features = false, registry = "strafesnet" }
|
||||
rbx_binary = "2.0.1"
|
||||
rbx_dom_weak = "4.1.0"
|
||||
|
||||
@@ -7,7 +7,6 @@ edition = "2024"
|
||||
async-nats.workspace = true
|
||||
aws-config = { version = "1", features = ["behavior-version-latest"] }
|
||||
aws-sdk-s3 = "1"
|
||||
futures-util.workspace = true
|
||||
map-tool = { version = "3.0.0", registry = "strafesnet", features = ["roblox"], default-features = false }
|
||||
rbx_asset.workspace = true
|
||||
rbx_binary.workspace = true
|
||||
|
||||
@@ -3,12 +3,8 @@ use std::io::Cursor;
|
||||
use crate::nats_types::ReleaseMapfixRequest;
|
||||
use crate::s3::S3Cache;
|
||||
|
||||
use futures_util::stream::iter as stream_iter;
|
||||
use futures_util::{StreamExt,TryStreamExt};
|
||||
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
|
||||
|
||||
const CONCURRENT_REQUESTS:usize=16;
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum ConvertError{
|
||||
@@ -69,6 +65,9 @@ pub enum Error{
|
||||
ArchivedModel,
|
||||
LoadDom(map_tool::roblox::LoadDomError),
|
||||
DownloadAsset(map_tool::roblox::DownloadAssetError),
|
||||
ConvertTexture(map_tool::roblox::ConvertTextureError),
|
||||
Union(rbx_binary::DecodeError),
|
||||
Mesh(strafesnet_rbx_loader::mesh::Error),
|
||||
ConvertSnf(ConvertError),
|
||||
S3Get(crate::s3::GetError),
|
||||
S3Put(crate::s3::PutError),
|
||||
@@ -95,9 +94,6 @@ impl Processor{
|
||||
if s.contains("Requested asset is archived"){
|
||||
println!("[combobulator] Asset {asset_id} is archived, skipping");
|
||||
Ok(None)
|
||||
}else if s.contains("User is not authorized to access Asset"){
|
||||
println!("[combobulator] User is not authorized to access Asset {asset_id}, skipping");
|
||||
Ok(None)
|
||||
}else if s.contains("Asset is not approved for the requester"){
|
||||
println!("[combobulator] Asset {asset_id} is not approved for the requester, skipping");
|
||||
Ok(None)
|
||||
@@ -125,9 +121,10 @@ impl Processor{
|
||||
let assets=map_tool::roblox::get_unique_assets(&dom);
|
||||
|
||||
// place textures into 'loader'
|
||||
let texture_loader=crate::loader::TextureLoader::new();
|
||||
let mut texture_loader=crate::loader::TextureLoader::new();
|
||||
|
||||
// process textures: download, cache, convert to DDS
|
||||
let texture_loader=stream_iter(assets.textures).map(async|id|{
|
||||
for &id in &assets.textures{
|
||||
let asset_id=id.0;
|
||||
let dds_key=S3Cache::texture_dds_key(asset_id);
|
||||
|
||||
@@ -141,9 +138,7 @@ impl Processor{
|
||||
map_tool::roblox::convert_texture_to_dds(&data)
|
||||
}else{
|
||||
println!("[combobulator] Downloading texture {asset_id}");
|
||||
let Some(data)=self.download_asset(asset_id).await? else{
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(data)=self.download_asset(asset_id).await? else{continue};
|
||||
|
||||
// decode while we have ownership
|
||||
let dds_result=map_tool::roblox::convert_texture_to_dds(&data);
|
||||
@@ -152,14 +147,8 @@ impl Processor{
|
||||
dds_result
|
||||
};
|
||||
|
||||
// handle error after cacheing data
|
||||
let dds=match dds_result{
|
||||
Ok(dds)=>dds,
|
||||
Err(e)=>{
|
||||
println!("[combobulator] Texture {asset_id} convert error: {e}");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
// handle error after caching data
|
||||
let dds=dds_result.map_err(Error::ConvertTexture)?;
|
||||
|
||||
self.s3.put(&dds_key,dds.clone()).await.map_err(Error::S3Put)?;
|
||||
|
||||
@@ -167,19 +156,12 @@ impl Processor{
|
||||
};
|
||||
println!("[combobulator] Texture {asset_id} processed");
|
||||
|
||||
Ok(Some((id,dds)))
|
||||
})
|
||||
.buffer_unordered(CONCURRENT_REQUESTS)
|
||||
.try_fold(texture_loader,async|mut texture_loader,maybe_loaded_texture|{
|
||||
if let Some((id,dds))=maybe_loaded_texture{
|
||||
texture_loader.insert(id,dds);
|
||||
}
|
||||
Ok(texture_loader)
|
||||
}).await?;
|
||||
texture_loader.insert(id,dds);
|
||||
}
|
||||
|
||||
let mesh_loader=crate::loader::MeshLoader::new();
|
||||
let mut mesh_loader=crate::loader::MeshLoader::new();
|
||||
// process meshes
|
||||
let mesh_loader=stream_iter(assets.meshes).map(async|id|{
|
||||
for &id in &assets.meshes{
|
||||
let asset_id=id.0;
|
||||
let mesh_key=S3Cache::mesh_key(asset_id);
|
||||
|
||||
@@ -187,9 +169,7 @@ impl Processor{
|
||||
strafesnet_rbx_loader::mesh::convert(&data)
|
||||
}else{
|
||||
println!("[combobulator] Downloading mesh {asset_id}");
|
||||
let Some(data)=self.download_asset(asset_id).await? else{
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(data)=self.download_asset(asset_id).await? else{continue};
|
||||
|
||||
// decode while we have ownership
|
||||
let mesh_result=strafesnet_rbx_loader::mesh::convert(&data);
|
||||
@@ -199,25 +179,14 @@ impl Processor{
|
||||
};
|
||||
println!("[combobulator] Mesh {asset_id} processed");
|
||||
|
||||
// handle error after cacheing data
|
||||
match mesh_result{
|
||||
Ok(mesh)=>Ok(Some((id,mesh))),
|
||||
Err(e)=>{
|
||||
println!("[combobulator] Mesh {asset_id} convert error: {e}");
|
||||
Ok(None)
|
||||
},
|
||||
}
|
||||
})
|
||||
.buffer_unordered(CONCURRENT_REQUESTS)
|
||||
.try_fold(mesh_loader,async|mut mesh_loader,maybe_loaded_mesh|{
|
||||
if let Some((id,mesh))=maybe_loaded_mesh{
|
||||
mesh_loader.insert_mesh(id,mesh);
|
||||
}
|
||||
Ok(mesh_loader)
|
||||
}).await?;
|
||||
// handle error after caching data
|
||||
let mesh=mesh_result.map_err(Error::Mesh)?;
|
||||
|
||||
mesh_loader.insert_mesh(id,mesh);
|
||||
}
|
||||
|
||||
// process unions
|
||||
let mesh_loader=stream_iter(assets.unions).map(async|id|{
|
||||
for &id in &assets.unions{
|
||||
let asset_id=id.0;
|
||||
let union_key=S3Cache::union_key(asset_id);
|
||||
|
||||
@@ -225,9 +194,7 @@ impl Processor{
|
||||
rbx_binary::from_reader(data.as_slice())
|
||||
}else{
|
||||
println!("[combobulator] Downloading union {asset_id}");
|
||||
let Some(data)=self.download_asset(asset_id).await? else{
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(data)=self.download_asset(asset_id).await? else{continue};
|
||||
|
||||
// decode the data while we have ownership
|
||||
let union_result=rbx_binary::from_reader(data.as_slice());
|
||||
@@ -237,22 +204,11 @@ impl Processor{
|
||||
};
|
||||
println!("[combobulator] Union {asset_id} processed");
|
||||
|
||||
// handle error after cacheing data
|
||||
match union_result{
|
||||
Ok(union)=>Ok(Some((id,union))),
|
||||
Err(e)=>{
|
||||
println!("[combobulator] Union {asset_id} convert error: {e}");
|
||||
Ok(None)
|
||||
},
|
||||
}
|
||||
})
|
||||
.buffer_unordered(CONCURRENT_REQUESTS)
|
||||
.try_fold(mesh_loader,async|mut mesh_loader,maybe_loaded_union|{
|
||||
if let Some((id,union))=maybe_loaded_union{
|
||||
mesh_loader.insert_union(id,union);
|
||||
}
|
||||
Ok(mesh_loader)
|
||||
}).await?;
|
||||
// handle error after caching data
|
||||
let union=union_result.map_err(Error::Union)?;
|
||||
|
||||
mesh_loader.insert_union(id,union);
|
||||
}
|
||||
|
||||
// convert to SNF and upload
|
||||
println!("[combobulator] Converting to SNF");
|
||||
|
||||
@@ -123,7 +123,7 @@ func (svc *Service) SeedCombobulator(ctx context.Context) error {
|
||||
//
|
||||
// Queue a map for combobulator processing.
|
||||
//
|
||||
// POST /maps/{MapID}/combobulate
|
||||
// POST /maps-admin/combobulate
|
||||
func (svc *Service) CombobulateMap(ctx context.Context, params api.CombobulateMapParams) error {
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
|
||||
@@ -12,35 +12,35 @@ import (
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/service"
|
||||
)
|
||||
|
||||
var (
|
||||
CreationPhaseSubmissionsLimit = 20
|
||||
var(
|
||||
CreationPhaseSubmissionsLimit = 20
|
||||
CreationPhaseSubmissionStatuses = []model.SubmissionStatus{
|
||||
model.SubmissionStatusChangesRequested,
|
||||
model.SubmissionStatusSubmitted,
|
||||
model.SubmissionStatusUnderConstruction,
|
||||
}
|
||||
// Allow 5 submissions every 10 minutes
|
||||
CreateSubmissionRateLimit int64 = 5
|
||||
CreateSubmissionRecencyWindow = time.Second * 600
|
||||
CreateSubmissionRateLimit int64 = 5
|
||||
CreateSubmissionRecencyWindow = time.Second*600
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCreationPhaseSubmissionsLimit = fmt.Errorf("%w: Active submissions limited to 20", ErrPermissionDenied)
|
||||
ErrUploadedAssetIDAlreadyExists = fmt.Errorf("%w: The submission UploadedAssetID is already set", ErrPermissionDenied)
|
||||
ErrReleaseInvalidStatus = fmt.Errorf("%w: Only submissions with Uploaded status can be released", ErrPermissionDenied)
|
||||
ErrReleaseNoUploadedAssetID = fmt.Errorf("%w: Only submissions with a UploadedAssetID can be released", ErrPermissionDenied)
|
||||
ErrAcceptOwnSubmission = fmt.Errorf("%w: You cannot accept your own submission as the submitter", ErrPermissionDenied)
|
||||
ErrCreateSubmissionRateLimit = fmt.Errorf("%w: You must not create more than 5 submissions every 10 minutes", ErrTooManyRequests)
|
||||
ErrDisplayNameNotUnique = fmt.Errorf("%w: Cannot submit: A map exists with the same DisplayName", ErrPermissionDenied)
|
||||
ErrUploadedAssetIDAlreadyExists = fmt.Errorf("%w: The submission UploadedAssetID is already set", ErrPermissionDenied)
|
||||
ErrReleaseInvalidStatus = fmt.Errorf("%w: Only submissions with Uploaded status can be released", ErrPermissionDenied)
|
||||
ErrReleaseNoUploadedAssetID = fmt.Errorf("%w: Only submissions with a UploadedAssetID can be released", ErrPermissionDenied)
|
||||
ErrAcceptOwnSubmission = fmt.Errorf("%w: You cannot accept your own submission as the submitter", ErrPermissionDenied)
|
||||
ErrCreateSubmissionRateLimit = fmt.Errorf("%w: You must not create more than 5 submissions every 10 minutes", ErrTooManyRequests)
|
||||
ErrDisplayNameNotUnique = fmt.Errorf("%w: Cannot submit: A map exists with the same DisplayName", ErrPermissionDenied)
|
||||
)
|
||||
|
||||
// POST /submissions
|
||||
func (svc *Service) CreateSubmission(ctx context.Context, request *api.SubmissionTriggerCreate) (*api.OperationID, error) {
|
||||
// sanitization
|
||||
if request.AssetID < 0 {
|
||||
if request.AssetID<0{
|
||||
return nil, ErrNegativeID
|
||||
}
|
||||
var ModelID = uint64(request.AssetID)
|
||||
var ModelID=uint64(request.AssetID);
|
||||
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
@@ -60,7 +60,7 @@ func (svc *Service) CreateSubmission(ctx context.Context, request *api.Submissio
|
||||
creation_submissions, err := svc.inner.ListSubmissions(ctx, filter, model.Page{
|
||||
Number: 1,
|
||||
Size: int32(CreationPhaseSubmissionsLimit),
|
||||
}, datastore.ListSortDisabled)
|
||||
},datastore.ListSortDisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -86,8 +86,8 @@ func (svc *Service) CreateSubmission(ctx context.Context, request *api.Submissio
|
||||
}
|
||||
|
||||
operation, err := svc.inner.CreateOperation(ctx, model.Operation{
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -110,14 +110,13 @@ func (svc *Service) CreateSubmission(ctx context.Context, request *api.Submissio
|
||||
OperationID: operation.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// POST /submissions-admin
|
||||
func (svc *Service) CreateSubmissionAdmin(ctx context.Context, request *api.SubmissionTriggerCreate) (*api.OperationID, error) {
|
||||
// sanitization
|
||||
if request.AssetID < 0 {
|
||||
if request.AssetID<0{
|
||||
return nil, ErrNegativeID
|
||||
}
|
||||
var ModelID = uint64(request.AssetID)
|
||||
var ModelID=uint64(request.AssetID);
|
||||
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
@@ -135,7 +134,7 @@ func (svc *Service) CreateSubmissionAdmin(ctx context.Context, request *api.Subm
|
||||
}
|
||||
|
||||
// check if caller has required role
|
||||
has_role := roles&model.RolesSubmissionReview == model.RolesSubmissionReview
|
||||
has_role := roles & model.RolesSubmissionReview == model.RolesSubmissionReview
|
||||
if !has_role {
|
||||
return nil, ErrPermissionDeniedNeedRoleSubmissionReview
|
||||
}
|
||||
@@ -156,8 +155,8 @@ func (svc *Service) CreateSubmissionAdmin(ctx context.Context, request *api.Subm
|
||||
}
|
||||
|
||||
operation, err := svc.inner.CreateOperation(ctx, model.Operation{
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -192,18 +191,18 @@ func (svc *Service) GetSubmission(ctx context.Context, params api.GetSubmissionP
|
||||
return nil, err
|
||||
}
|
||||
return &api.Submission{
|
||||
ID: submission.ID,
|
||||
DisplayName: submission.DisplayName,
|
||||
Creator: submission.Creator,
|
||||
GameID: int32(submission.GameID),
|
||||
CreatedAt: submission.CreatedAt.Unix(),
|
||||
UpdatedAt: submission.UpdatedAt.Unix(),
|
||||
Submitter: int64(submission.Submitter),
|
||||
AssetID: int64(submission.AssetID),
|
||||
AssetVersion: int64(submission.AssetVersion),
|
||||
Completed: submission.Completed,
|
||||
ID: submission.ID,
|
||||
DisplayName: submission.DisplayName,
|
||||
Creator: submission.Creator,
|
||||
GameID: int32(submission.GameID),
|
||||
CreatedAt: submission.CreatedAt.Unix(),
|
||||
UpdatedAt: submission.UpdatedAt.Unix(),
|
||||
Submitter: int64(submission.Submitter),
|
||||
AssetID: int64(submission.AssetID),
|
||||
AssetVersion: int64(submission.AssetVersion),
|
||||
Completed: submission.Completed,
|
||||
UploadedAssetID: api.NewOptInt64(int64(submission.UploadedAssetID)),
|
||||
StatusID: int32(submission.StatusID),
|
||||
StatusID: int32(submission.StatusID),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -215,28 +214,28 @@ func (svc *Service) GetSubmission(ctx context.Context, params api.GetSubmissionP
|
||||
func (svc *Service) ListSubmissions(ctx context.Context, params api.ListSubmissionsParams) (*api.Submissions, error) {
|
||||
filter := service.NewSubmissionFilter()
|
||||
|
||||
if display_name, display_name_ok := params.DisplayName.Get(); display_name_ok {
|
||||
if display_name, display_name_ok := params.DisplayName.Get(); display_name_ok{
|
||||
filter.SetDisplayName(display_name)
|
||||
}
|
||||
if creator, creator_ok := params.Creator.Get(); creator_ok {
|
||||
if creator, creator_ok := params.Creator.Get(); creator_ok{
|
||||
filter.SetCreator(creator)
|
||||
}
|
||||
if game_id, game_id_ok := params.GameID.Get(); game_id_ok {
|
||||
if game_id, game_id_ok := params.GameID.Get(); game_id_ok{
|
||||
filter.SetGameID(uint32(game_id))
|
||||
}
|
||||
if submitter, submitter_ok := params.Submitter.Get(); submitter_ok {
|
||||
if submitter, submitter_ok := params.Submitter.Get(); submitter_ok{
|
||||
filter.SetSubmitter(uint64(submitter))
|
||||
}
|
||||
if asset_id, asset_id_ok := params.AssetID.Get(); asset_id_ok {
|
||||
if asset_id, asset_id_ok := params.AssetID.Get(); asset_id_ok{
|
||||
filter.SetAssetID(uint64(asset_id))
|
||||
}
|
||||
if asset_version, asset_version_ok := params.AssetVersion.Get(); asset_version_ok {
|
||||
if asset_version, asset_version_ok := params.AssetVersion.Get(); asset_version_ok{
|
||||
filter.SetAssetVersion(uint64(asset_version))
|
||||
}
|
||||
if uploaded_asset_id, uploaded_asset_id_ok := params.UploadedAssetID.Get(); uploaded_asset_id_ok {
|
||||
if uploaded_asset_id, uploaded_asset_id_ok := params.UploadedAssetID.Get(); uploaded_asset_id_ok{
|
||||
filter.SetUploadedAssetID(uint64(uploaded_asset_id))
|
||||
}
|
||||
if status_id, status_id_ok := params.StatusID.Get(); status_id_ok {
|
||||
if status_id, status_id_ok := params.StatusID.Get(); status_id_ok{
|
||||
filter.SetStatuses([]model.SubmissionStatus{model.SubmissionStatus(status_id)})
|
||||
}
|
||||
|
||||
@@ -245,27 +244,27 @@ func (svc *Service) ListSubmissions(ctx context.Context, params api.ListSubmissi
|
||||
total, items, err := svc.inner.ListSubmissionsWithTotal(ctx, filter, model.Page{
|
||||
Number: params.Page,
|
||||
Size: params.Limit,
|
||||
}, sort)
|
||||
},sort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp api.Submissions
|
||||
resp.Total = total
|
||||
resp.Total=total
|
||||
for _, item := range items {
|
||||
resp.Submissions = append(resp.Submissions, api.Submission{
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Creator: item.Creator,
|
||||
GameID: int32(item.GameID),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
Submitter: int64(item.Submitter),
|
||||
AssetID: int64(item.AssetID),
|
||||
AssetVersion: int64(item.AssetVersion),
|
||||
Completed: item.Completed,
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Creator: item.Creator,
|
||||
GameID: int32(item.GameID),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
Submitter: int64(item.Submitter),
|
||||
AssetID: int64(item.AssetID),
|
||||
AssetVersion: int64(item.AssetVersion),
|
||||
Completed: item.Completed,
|
||||
UploadedAssetID: api.NewOptInt64(int64(item.UploadedAssetID)),
|
||||
StatusID: int32(item.StatusID),
|
||||
StatusID: int32(item.StatusID),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -342,9 +341,9 @@ func (svc *Service) UpdateSubmissionModel(ctx context.Context, params api.Update
|
||||
}
|
||||
|
||||
event_data := model.AuditEventDataChangeModel{
|
||||
OldModelID: OldModelID,
|
||||
OldModelID: OldModelID,
|
||||
OldModelVersion: OldModelVersion,
|
||||
NewModelID: NewModelID,
|
||||
NewModelID: NewModelID,
|
||||
NewModelVersion: NewModelVersion,
|
||||
}
|
||||
|
||||
@@ -352,7 +351,7 @@ func (svc *Service) UpdateSubmissionModel(ctx context.Context, params api.Update
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -402,7 +401,7 @@ func (svc *Service) ActionSubmissionReject(ctx context.Context, params api.Actio
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -457,7 +456,7 @@ func (svc *Service) ActionSubmissionRequestChanges(ctx context.Context, params a
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -510,7 +509,7 @@ func (svc *Service) ActionSubmissionRevoke(ctx context.Context, params api.Actio
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -567,12 +566,8 @@ func (svc *Service) ActionSubmissionTriggerSubmit(ctx context.Context, params ap
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The map search finds substrings, we only want exact matches
|
||||
for _, m := range maps_list {
|
||||
if m.DisplayName == submission.DisplayName {
|
||||
return ErrDisplayNameNotUnique
|
||||
}
|
||||
if len(maps_list) != 0 {
|
||||
return ErrDisplayNameNotUnique
|
||||
}
|
||||
|
||||
// transaction
|
||||
@@ -606,7 +601,7 @@ func (svc *Service) ActionSubmissionTriggerSubmit(ctx context.Context, params ap
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -677,7 +672,7 @@ func (svc *Service) ActionSubmissionTriggerSubmitUnchecked(ctx context.Context,
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -705,7 +700,7 @@ func (svc *Service) ActionSubmissionResetSubmitting(ctx context.Context, params
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second * 10)) {
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) {
|
||||
// the last time the submission was updated must be longer than 10 seconds ago
|
||||
return ErrDelayReset
|
||||
}
|
||||
@@ -734,7 +729,7 @@ func (svc *Service) ActionSubmissionResetSubmitting(ctx context.Context, params
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -802,7 +797,7 @@ func (svc *Service) ActionSubmissionTriggerUpload(ctx context.Context, params ap
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -839,7 +834,7 @@ func (svc *Service) ActionSubmissionValidated(ctx context.Context, params api.Ac
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second * 10)) {
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) {
|
||||
// the last time the submission was updated must be longer than 10 seconds ago
|
||||
return ErrDelayReset
|
||||
}
|
||||
@@ -862,7 +857,7 @@ func (svc *Service) ActionSubmissionValidated(ctx context.Context, params api.Ac
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -934,7 +929,7 @@ func (svc *Service) ActionSubmissionTriggerValidate(ctx context.Context, params
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -994,7 +989,7 @@ func (svc *Service) ActionSubmissionRetryValidate(ctx context.Context, params ap
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -1031,7 +1026,7 @@ func (svc *Service) ActionSubmissionAccepted(ctx context.Context, params api.Act
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second * 10)) {
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) {
|
||||
// the last time the submission was updated must be longer than 10 seconds ago
|
||||
return ErrDelayReset
|
||||
}
|
||||
@@ -1054,7 +1049,7 @@ func (svc *Service) ActionSubmissionAccepted(ctx context.Context, params api.Act
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -1101,11 +1096,11 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas
|
||||
id_to_submission := make(map[int64]*model.Submission, len(submissions))
|
||||
|
||||
// check each submission to make sure it is ready to release
|
||||
for _, submission := range submissions {
|
||||
if submission.StatusID != model.SubmissionStatusUploaded {
|
||||
for _,submission := range submissions{
|
||||
if submission.StatusID != model.SubmissionStatusUploaded{
|
||||
return nil, ErrReleaseInvalidStatus
|
||||
}
|
||||
if submission.UploadedAssetID == 0 {
|
||||
if submission.UploadedAssetID == 0{
|
||||
return nil, ErrReleaseNoUploadedAssetID
|
||||
}
|
||||
id_to_submission[submission.ID] = &submission
|
||||
@@ -1131,8 +1126,8 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas
|
||||
|
||||
// create a trackable long-running operation
|
||||
operation, err := svc.inner.CreateOperation(ctx, model.Operation{
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1154,10 +1149,10 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas
|
||||
|
||||
// CreateSubmissionAuditComment implements createSubmissionAuditComment operation.
|
||||
//
|
||||
// # Post a comment to the audit log
|
||||
// Post a comment to the audit log
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/comment
|
||||
func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.CreateSubmissionAuditCommentReq, params api.CreateSubmissionAuditCommentParams) error {
|
||||
func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.CreateSubmissionAuditCommentReq, params api.CreateSubmissionAuditCommentParams) (error) {
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
return ErrUserInfo
|
||||
@@ -1198,7 +1193,7 @@ func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.Cr
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -1214,7 +1209,7 @@ func (svc *Service) ListSubmissionAuditEvents(ctx context.Context, params api.Li
|
||||
return svc.inner.ListAuditEvents(
|
||||
ctx,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
model.Page{
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-nats.workspace = true
|
||||
futures-util.workspace = true
|
||||
futures = "0.3.31"
|
||||
rbx_asset.workspace = true
|
||||
rbx_binary.workspace = true
|
||||
rbx_dom_weak.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use futures_util::StreamExt;
|
||||
use futures::StreamExt;
|
||||
|
||||
mod download;
|
||||
mod grpc;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use futures_util::stream::iter as stream_iter;
|
||||
use futures_util::StreamExt;
|
||||
use futures::StreamExt;
|
||||
|
||||
use crate::download::download_asset_version;
|
||||
use crate::nats_types::ReleaseSubmissionsBatchRequest;
|
||||
@@ -93,7 +92,7 @@ async fn release_inner(
|
||||
.collect();
|
||||
|
||||
// fut_download
|
||||
let fut_download=stream_iter(asset_versions)
|
||||
let fut_download=futures::stream::iter(asset_versions)
|
||||
.map(|(index,asset_version)|async move{
|
||||
let modes=download_fut(cloud_context,asset_version).await;
|
||||
(index,modes)
|
||||
@@ -138,7 +137,7 @@ async fn release_inner(
|
||||
}
|
||||
|
||||
// concurrently dispatch results
|
||||
let release_results:Vec<_> =stream_iter(
|
||||
let release_results:Vec<_> =futures::stream::iter(
|
||||
release_info
|
||||
.Submissions
|
||||
.into_iter()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use futures_util::stream::iter as stream_iter;
|
||||
use futures_util::TryStreamExt;
|
||||
use futures::TryStreamExt;
|
||||
use rust_grpc::validator::Policy;
|
||||
|
||||
use crate::download::download_asset_version;
|
||||
@@ -154,7 +153,7 @@ impl crate::message_handler::MessageHandler{
|
||||
}
|
||||
|
||||
// send all script hashes to REST endpoint and retrieve the replacements
|
||||
stream_iter(script_map.iter_mut().map(Ok))
|
||||
futures::stream::iter(script_map.iter_mut().map(Ok))
|
||||
.try_for_each_concurrent(Some(SCRIPT_CONCURRENCY),|(source,NamePolicy{policy,name})|async{
|
||||
// get the hash
|
||||
let hash=hash_source(source.as_str());
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
<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>StrafesNET | Maps</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Barlow:wght@700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background-color: #09090b; }
|
||||
</style>
|
||||
<title>Maps Service</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import { ThemeProvider, CssBaseline } from '@mui/material'
|
||||
import { ThemeProvider } from '@mui/material'
|
||||
import { theme } from '@/app/lib/theme'
|
||||
|
||||
// Pages
|
||||
@@ -22,7 +22,6 @@ import NotFound from '@/app/not-found/page'
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/maps" element={<MapsPage />} />
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { surface } from '@/app/lib/colors';
|
||||
|
||||
const AnimatedBackground: React.FC = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 0,
|
||||
overflow: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
background: `
|
||||
radial-gradient(ellipse 80% 60% at 50% -20%, rgba(124, 58, 237, 0.08) 0%, transparent 100%),
|
||||
radial-gradient(ellipse 60% 40% at 80% 80%, rgba(34, 211, 238, 0.04) 0%, transparent 100%),
|
||||
${surface.base}
|
||||
`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
opacity: 0.03,
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: '128px 128px',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimatedBackground;
|
||||
@@ -75,7 +75,7 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(167, 139, 250, 0.08)',
|
||||
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -93,12 +93,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 2,
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid rgba(167, 139, 250, 0.15)',
|
||||
border: '1px solid rgba(99, 102, 241, 0.2)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'background.paper',
|
||||
borderColor: 'rgba(167, 139, 250, 0.3)',
|
||||
boxShadow: '0 8px 20px rgba(167, 139, 250, 0.2)',
|
||||
borderColor: 'rgba(99, 102, 241, 0.4)',
|
||||
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
|
||||
},
|
||||
visibility: scrollPosition <= 5 ? 'hidden' : 'visible',
|
||||
}}
|
||||
@@ -146,12 +146,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 2,
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid rgba(167, 139, 250, 0.15)',
|
||||
border: '1px solid rgba(99, 102, 241, 0.2)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'background.paper',
|
||||
borderColor: 'rgba(167, 139, 250, 0.3)',
|
||||
boxShadow: '0 8px 20px rgba(167, 139, 250, 0.2)',
|
||||
borderColor: 'rgba(99, 102, 241, 0.4)',
|
||||
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
|
||||
},
|
||||
visibility: scrollPosition >= maxScroll - 5 ? 'hidden' : 'visible',
|
||||
}}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Tab,
|
||||
keyframes
|
||||
} from "@mui/material";
|
||||
import { semantic } from "@/app/lib/colors";
|
||||
import CommentsTabPanel from './CommentsTabPanel';
|
||||
import AuditEventsTabPanel from './AuditEventsTabPanel';
|
||||
import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
|
||||
@@ -76,7 +75,7 @@ export default function CommentsAndAuditSection({
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: semantic.warning,
|
||||
backgroundColor: '#ff9800',
|
||||
animation: `${pulse} 2s ease-in-out infinite`
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import { useState } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { useUser } from "@/app/hooks/useUser";
|
||||
import { primary, text, border, fill } from "@/app/lib/colors";
|
||||
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
@@ -11,44 +10,51 @@ import Box from "@mui/material/Box";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import Drawer from "@mui/material/Drawer";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import LoginIcon from "@mui/icons-material/Login";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
|
||||
const navItems = [
|
||||
interface HeaderButton {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
const navItems: HeaderButton[] = [
|
||||
{ name: "Home", href: "/" },
|
||||
{ name: "Submissions", href: "/submissions" },
|
||||
{ name: "Mapfixes", href: "/mapfixes" },
|
||||
{ name: "Maps", href: "/maps" },
|
||||
];
|
||||
|
||||
const quickLinks = [
|
||||
{ name: "Bhop", href: "https://www.roblox.com/games/5315046213" },
|
||||
{ name: "Bhop Maptest", href: "https://www.roblox.com/games/517201717" },
|
||||
{ name: "Surf", href: "https://www.roblox.com/games/5315066937" },
|
||||
{ name: "Surf Maptest", href: "https://www.roblox.com/games/517206177" },
|
||||
{ name: "Fly Trials", href: "https://www.roblox.com/games/12591611759" },
|
||||
{ name: "Fly Trials Maptest", href: "https://www.roblox.com/games/12724901535" },
|
||||
];
|
||||
function HeaderButton(header: HeaderButton) {
|
||||
return (
|
||||
<Button color="inherit" component={Link} to={header.href}>
|
||||
{header.name}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const hasAnimated = useRef(false);
|
||||
|
||||
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';
|
||||
};
|
||||
|
||||
@@ -80,20 +86,9 @@ export default function Header() {
|
||||
setQuickLinksAnchor(null);
|
||||
};
|
||||
|
||||
// Mobile navigation drawer content
|
||||
const drawer = (
|
||||
<Box onClick={handleDrawerToggle} sx={{ textAlign: 'center', pt: 2 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontFamily: '"Barlow", sans-serif',
|
||||
fontWeight: 700,
|
||||
color: text.primary,
|
||||
mb: 2,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
StrafesNET
|
||||
</Typography>
|
||||
<Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}>
|
||||
<List>
|
||||
{navItems.map((item) => (
|
||||
<ListItem key={item.name} disablePadding>
|
||||
@@ -105,7 +100,7 @@ export default function Header() {
|
||||
{isLoggedIn && user && (
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to="/submit" sx={{ textAlign: 'center' }}>
|
||||
<ListItemText primary="Submit Map" sx={{ color: primary.main }} />
|
||||
<ListItemText primary="Submit Map" sx={{ color: 'success.main' }} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)}
|
||||
@@ -127,88 +122,125 @@ export default function Header() {
|
||||
</Box>
|
||||
);
|
||||
|
||||
const quickLinks = [
|
||||
{ name: "Bhop", href: "https://www.roblox.com/games/5315046213" },
|
||||
{ name: "Bhop Maptest", href: "https://www.roblox.com/games/517201717" },
|
||||
{ name: "Surf", href: "https://www.roblox.com/games/5315066937" },
|
||||
{ name: "Surf Maptest", href: "https://www.roblox.com/games/517206177" },
|
||||
{ name: "Fly Trials", href: "https://www.roblox.com/games/12591611759" },
|
||||
{ name: "Fly Trials Maptest", href: "https://www.roblox.com/games/12724901535" },
|
||||
];
|
||||
|
||||
return (
|
||||
<AppBar position="sticky">
|
||||
<Toolbar sx={{ gap: 1 }}>
|
||||
<AppBar position="static">
|
||||
<Toolbar sx={{ py: 1 }}>
|
||||
{isMobile && (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
edge="start"
|
||||
onClick={handleDrawerToggle}
|
||||
sx={{ color: primary.main }}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{/* Brand */}
|
||||
{/* Desktop navigation */}
|
||||
{!isMobile && (
|
||||
<Box
|
||||
component={Link}
|
||||
to="/"
|
||||
sx={{
|
||||
mr: 4,
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
<Box display="flex" flexGrow={1} gap={1} alignItems="center">
|
||||
{/* Logo/Brand */}
|
||||
<Box
|
||||
component={Link}
|
||||
to="/"
|
||||
sx={{
|
||||
fontFamily: '"Barlow", sans-serif',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.02em',
|
||||
color: text.primary,
|
||||
lineHeight: 1,
|
||||
userSelect: 'none',
|
||||
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,
|
||||
},
|
||||
}}
|
||||
>
|
||||
StrafesNET
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
color: text.muted,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
Maps
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isMobile && (
|
||||
<Box
|
||||
component={Link}
|
||||
to="/"
|
||||
sx={{ textDecoration: 'none', display: 'flex', alignItems: 'baseline', gap: 0.75 }}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontFamily: '"Barlow", sans-serif',
|
||||
fontWeight: 700,
|
||||
color: text.primary,
|
||||
lineHeight: 1,
|
||||
userSelect: 'none',
|
||||
fontSize: '1rem',
|
||||
}}
|
||||
>
|
||||
StrafesNET
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 500, color: text.muted, userSelect: 'none' }}>
|
||||
Maps
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Desktop nav items */}
|
||||
{!isMobile && (
|
||||
<>
|
||||
<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) => (
|
||||
<Button
|
||||
key={item.name}
|
||||
@@ -216,157 +248,192 @@ export default function Header() {
|
||||
component={Link}
|
||||
to={item.href}
|
||||
sx={{
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
borderRadius: 2,
|
||||
fontSize: '0.85rem',
|
||||
px: 2,
|
||||
py: 1,
|
||||
borderRadius: 1.5,
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 500,
|
||||
color: text.tertiary,
|
||||
transition: 'all 0.15s ease',
|
||||
color: 'text.secondary',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryHover,
|
||||
color: text.primary,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||
color: 'text.primary',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{ flex: 1 }} />
|
||||
|
||||
{/* Quick Links Dropdown */}
|
||||
{!isMobile && (
|
||||
<Box>
|
||||
<Button
|
||||
color="inherit"
|
||||
endIcon={<ArrowDropDownIcon />}
|
||||
onClick={handleQuickLinksOpen}
|
||||
sx={{
|
||||
px: 1.5,
|
||||
borderRadius: 2,
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 500,
|
||||
color: text.dim,
|
||||
transition: 'all 0.15s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryHover,
|
||||
color: text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Quick Links
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={quickLinksAnchor}
|
||||
open={Boolean(quickLinksAnchor)}
|
||||
onClose={handleQuickLinksClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
sx={{ '& .MuiMenu-paper': { mt: 1 } }}
|
||||
>
|
||||
{quickLinks.map(link => (
|
||||
<MenuItem
|
||||
key={link.name}
|
||||
onClick={handleQuickLinksClose}
|
||||
sx={{ minWidth: 200, fontSize: '0.85rem' }}
|
||||
component="a"
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{link.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Submit + Auth */}
|
||||
{!isMobile && isLoggedIn && user && (
|
||||
<Button
|
||||
variant="contained"
|
||||
component={Link}
|
||||
to="/submit"
|
||||
size="small"
|
||||
sx={{ px: 2 }}
|
||||
>
|
||||
Submit Map
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isLoggedIn && user ? (
|
||||
<Box display="flex" alignItems="center">
|
||||
<IconButton
|
||||
onClick={handleMenuOpen}
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
{/* Quick Links Dropdown */}
|
||||
<Box>
|
||||
<Button
|
||||
color="inherit"
|
||||
endIcon={<ArrowDropDownIcon />}
|
||||
onClick={handleQuickLinksOpen}
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
p: 0.5,
|
||||
transition: 'all 0.15s ease',
|
||||
px: 2,
|
||||
mr: 1,
|
||||
borderRadius: 1.5,
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 500,
|
||||
color: 'text.secondary',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
boxShadow: '0 0 12px rgba(167, 139, 250, 0.3)',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||
color: 'text.primary',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={user.AvatarURL}
|
||||
>
|
||||
Quick Links
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={quickLinksAnchor}
|
||||
open={Boolean(quickLinksAnchor)}
|
||||
onClose={handleQuickLinksClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 700,
|
||||
backgroundColor: primary.dark,
|
||||
color: text.primary,
|
||||
'& .MuiMenu-paper': {
|
||||
mt: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{user.Username?.slice(0, 2).toUpperCase()}
|
||||
</Avatar>
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
sx={{ '& .MuiMenu-paper': { mt: 1 } }}
|
||||
>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={getAuthUrl()}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
>
|
||||
Manage Account
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
{quickLinks.map(link => (
|
||||
<MenuItem
|
||||
key={link.name}
|
||||
onClick={handleQuickLinksClose}
|
||||
sx={{
|
||||
minWidth: 200,
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
component="a"
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{link.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleLoginClick}
|
||||
sx={{
|
||||
ml: 1,
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
minWidth: 0,
|
||||
borderRadius: 2,
|
||||
backgroundColor: fill.default,
|
||||
border: `1px solid ${border.default}`,
|
||||
color: text.dim,
|
||||
fontSize: '0.8rem',
|
||||
transition: 'all 0.15s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.hover,
|
||||
borderColor: border.primaryMedium,
|
||||
color: primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LoginIcon sx={{ fontSize: 16, mr: 0.5 }} />
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Spacer for mobile view */}
|
||||
{isMobile && <Box sx={{ flexGrow: 1 }} />}
|
||||
|
||||
{/* Right side of nav */}
|
||||
<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 && isLoggedIn && user ? (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Button
|
||||
onClick={handleMenuOpen}
|
||||
color="inherit"
|
||||
size="small"
|
||||
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)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className="avatar"
|
||||
width={28}
|
||||
height={28}
|
||||
src={user.AvatarURL}
|
||||
alt={user.Username}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.875rem', fontWeight: 500 }}>
|
||||
{user.Username}
|
||||
</Typography>
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiMenu-paper': {
|
||||
mt: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={getAuthUrl()}
|
||||
sx={{
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
>
|
||||
Manage Account
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
) : !isMobile && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleLoginClick}
|
||||
sx={{
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* In mobile view, display just the avatar if logged in */}
|
||||
{isMobile && isLoggedIn && user && (
|
||||
<IconButton
|
||||
onClick={handleMenuOpen}
|
||||
color="inherit"
|
||||
size="small"
|
||||
>
|
||||
<img
|
||||
className="avatar"
|
||||
width={32}
|
||||
height={32}
|
||||
src={user.AvatarURL}
|
||||
alt={user.Username}
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
|
||||
{/* Mobile drawer */}
|
||||
@@ -374,7 +441,9 @@ export default function Header() {
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiDrawer-paper': {
|
||||
boxSizing: 'border-box',
|
||||
@@ -386,4 +455,4 @@ export default function Header() {
|
||||
</Drawer>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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";
|
||||
import { primary, gameColors } from "@/app/lib/colors";
|
||||
|
||||
interface MapCardProps {
|
||||
displayName: string;
|
||||
@@ -120,13 +119,13 @@ export function MapCard(props: MapCardProps) {
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Explore sx={{ fontSize: '1rem', color: gameColors[props.gameID] || primary.main }} />
|
||||
<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: primary.main }} />
|
||||
<Person2 sx={{ fontSize: '1rem', color: '#8b5cf6' }} />
|
||||
{props.type === 'mapfix' && usernameLoading ? (
|
||||
<Skeleton variant="text" width={80} />
|
||||
) : (
|
||||
|
||||
@@ -6,7 +6,6 @@ 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';
|
||||
import { semantic } from "@/app/lib/colors";
|
||||
|
||||
const pulse = keyframes`
|
||||
0%, 100% {
|
||||
@@ -189,8 +188,8 @@ const WorkflowStepper: React.FC<WorkflowStepperProps> = ({ currentStatus, type,
|
||||
icon: InfoOutlinedIcon,
|
||||
title: 'Not Yet Submitted',
|
||||
message: 'Your submission has been created but has not been submitted. Click "Submit" to submit it.',
|
||||
color: semantic.info,
|
||||
bgColor: 'rgba(56, 189, 248, 0.08)'
|
||||
color: '#2196f3',
|
||||
bgColor: 'rgba(33, 150, 243, 0.08)'
|
||||
};
|
||||
}
|
||||
if (isChangesRequested) {
|
||||
@@ -198,8 +197,8 @@ const WorkflowStepper: React.FC<WorkflowStepperProps> = ({ currentStatus, type,
|
||||
icon: WarningIcon,
|
||||
title: 'Changes Requested',
|
||||
message: 'Review comments and audit events, make modifications, and submit again.',
|
||||
color: semantic.warning,
|
||||
bgColor: 'rgba(251, 191, 36, 0.08)'
|
||||
color: '#ff9800',
|
||||
bgColor: 'rgba(255, 152, 0, 0.08)'
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -1,76 +1,86 @@
|
||||
import {JSX} from "react";
|
||||
import {Cancel, CheckCircle, Pending} from "@mui/icons-material";
|
||||
import {Chip} from "@mui/material";
|
||||
import { semantic, text } from "@/app/lib/colors";
|
||||
|
||||
interface StatusConfig {
|
||||
bg: string;
|
||||
color: string;
|
||||
border: string;
|
||||
icon: JSX.Element;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function getStatusConfig(status: number): StatusConfig {
|
||||
const warn = {
|
||||
bg: `rgba(251, 191, 36, 0.08)`,
|
||||
color: semantic.warning,
|
||||
border: `rgba(251, 191, 36, 0.2)`,
|
||||
};
|
||||
const info = {
|
||||
bg: `rgba(56, 189, 248, 0.08)`,
|
||||
color: semantic.info,
|
||||
border: `rgba(56, 189, 248, 0.2)`,
|
||||
};
|
||||
const success = {
|
||||
bg: `rgba(74, 222, 128, 0.08)`,
|
||||
color: semantic.success,
|
||||
border: `rgba(74, 222, 128, 0.2)`,
|
||||
};
|
||||
const error = {
|
||||
bg: `rgba(248, 113, 113, 0.08)`,
|
||||
color: semantic.error,
|
||||
border: `rgba(248, 113, 113, 0.2)`,
|
||||
};
|
||||
const gray = {
|
||||
bg: `rgba(161, 161, 170, 0.08)`,
|
||||
color: text.tertiary,
|
||||
border: `rgba(161, 161, 170, 0.2)`,
|
||||
};
|
||||
|
||||
switch (status) {
|
||||
case 0: return { ...warn, icon: <Pending fontSize="small" />, label: 'Under Construction' };
|
||||
case 1: return { ...warn, icon: <Pending fontSize="small" />, label: 'Changes Requested' };
|
||||
case 2: return { ...info, icon: <Pending fontSize="small" />, label: 'Submitting' };
|
||||
case 3: return { ...warn, icon: <CheckCircle fontSize="small" />, label: 'Under Review' };
|
||||
case 4: return { ...warn, icon: <Pending fontSize="small" />, label: 'Script Review' };
|
||||
case 5: return { ...info, icon: <Pending fontSize="small" />, label: 'Validating' };
|
||||
case 6: return { ...success, icon: <CheckCircle fontSize="small" />, label: 'Validated' };
|
||||
case 7: return { ...info, icon: <Pending fontSize="small" />, label: 'Uploading' };
|
||||
case 8: return { ...success, icon: <CheckCircle fontSize="small" />, label: 'Uploaded' };
|
||||
case 9: return { ...error, icon: <Cancel fontSize="small" />, label: 'Rejected' };
|
||||
case 10: return { ...success, icon: <CheckCircle fontSize="small" />, label: 'Released' };
|
||||
case 11: return { ...info, icon: <Pending fontSize="small" />, label: 'Releasing' };
|
||||
default: return { ...gray, icon: <Pending fontSize="small" />, label: 'Unknown' };
|
||||
}
|
||||
}
|
||||
|
||||
export const StatusChip = ({status}: { status: number }): JSX.Element => {
|
||||
const config = getStatusConfig(status);
|
||||
let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default';
|
||||
let icon: JSX.Element = <Pending fontSize="small"/>;
|
||||
let label: string = 'Unknown';
|
||||
|
||||
switch (status) {
|
||||
case 0:
|
||||
color = 'warning';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Under Construction';
|
||||
break;
|
||||
case 1:
|
||||
color = 'warning';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Changes Requested';
|
||||
break;
|
||||
case 2:
|
||||
color = 'info';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Submitting';
|
||||
break;
|
||||
case 3:
|
||||
color = 'warning';
|
||||
icon = <CheckCircle fontSize="small"/>;
|
||||
label = 'Under Review';
|
||||
break;
|
||||
case 4:
|
||||
color = 'warning';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Script Review';
|
||||
break;
|
||||
case 5:
|
||||
color = 'info';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Validating';
|
||||
break;
|
||||
case 6:
|
||||
color = 'success';
|
||||
icon = <CheckCircle fontSize="small"/>;
|
||||
label = 'Validated';
|
||||
break;
|
||||
case 7:
|
||||
color = 'info';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Uploading';
|
||||
break;
|
||||
case 8:
|
||||
color = 'success';
|
||||
icon = <CheckCircle fontSize="small"/>;
|
||||
label = 'Uploaded';
|
||||
break;
|
||||
case 11:
|
||||
color = 'info';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Releasing';
|
||||
break;
|
||||
case 9:
|
||||
color = 'error';
|
||||
icon = <Cancel fontSize="small"/>;
|
||||
label = 'Rejected';
|
||||
break;
|
||||
case 10:
|
||||
color = 'success';
|
||||
icon = <CheckCircle fontSize="small"/>;
|
||||
label = 'Released';
|
||||
break;
|
||||
default:
|
||||
color = 'default';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Unknown';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Chip
|
||||
icon={config.icon}
|
||||
label={config.label}
|
||||
icon={icon}
|
||||
label={label}
|
||||
color={color}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: config.bg,
|
||||
color: config.color,
|
||||
border: `1px solid ${config.border}`,
|
||||
'& .MuiChip-icon': {
|
||||
color: config.color,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { Box } from "@mui/material";
|
||||
import Header from "./header";
|
||||
import AnimatedBackground from "./AnimatedBackground";
|
||||
|
||||
export default function Webpage({children}: Readonly<{children?: React.ReactNode}>) {
|
||||
return <>
|
||||
<AnimatedBackground />
|
||||
<Box sx={{ position: 'relative', zIndex: 1, minHeight: '100vh' }}>
|
||||
<Header/>
|
||||
{children}
|
||||
</Box>
|
||||
<Header/>
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
color: var(--text-color);
|
||||
}
|
||||
& fieldset {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgb(100,100,100);
|
||||
}
|
||||
& span {
|
||||
color: #fafafa;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
$review-border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
$review-border: 1px solid var(--review-border);
|
||||
$form-label-fontsize: 1.3rem;
|
||||
|
||||
@mixin border-with-radius {
|
||||
border: $review-border {
|
||||
radius: 8px;
|
||||
radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
--review-border: rgba(255, 255, 255, 0.06);
|
||||
--text-color: #fafafa;
|
||||
--placeholder-text: #52525b;
|
||||
--header-height: 45px;
|
||||
|
||||
--page: rgb(15,15,15);
|
||||
--header-grad-left: #363b40;
|
||||
--header-grad-right: #353a40;
|
||||
--header-button-left: white;
|
||||
--header-button-right: #b4b4b4;
|
||||
--header-button-hover: white;
|
||||
--review-border: rgb(50,50,50);
|
||||
--text-color: rgb(230,230,230);
|
||||
--anchor-link-review: #008fd6;
|
||||
--window-header: rgb(10,10,10);
|
||||
--comment-highlighted: #ffffd7;
|
||||
--comment-area: rgb(20,20,20);
|
||||
--placeholder-text: rgb(80,80,80);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla";
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
background-color: var(--page);
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -22,3 +41,10 @@ button {
|
||||
a:active, a:link, a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--review-border);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// Brand / accent
|
||||
export const primary = {
|
||||
main: '#a78bfa',
|
||||
light: '#c4b5fd',
|
||||
dark: '#7c3aed',
|
||||
darker: '#6d28d9',
|
||||
mid: '#8b5cf6',
|
||||
} as const;
|
||||
|
||||
export const secondary = {
|
||||
main: '#22d3ee',
|
||||
light: '#67e8f9',
|
||||
dark: '#0891b2',
|
||||
} as const;
|
||||
|
||||
// Semantic
|
||||
export const semantic = {
|
||||
error: '#f87171',
|
||||
warning: '#fbbf24',
|
||||
success: '#4ade80',
|
||||
info: '#38bdf8',
|
||||
} as const;
|
||||
|
||||
// Surfaces
|
||||
export const surface = {
|
||||
base: '#09090b',
|
||||
raised: '#18181b',
|
||||
raisedAlpha: 'rgba(24, 24, 27, 0.6)',
|
||||
raisedSolid: 'rgba(24, 24, 27, 0.95)',
|
||||
overlay: 'rgba(0, 0, 0, 0.6)',
|
||||
appBar: 'rgba(9, 9, 11, 0.8)',
|
||||
} as const;
|
||||
|
||||
// Text hierarchy (lightest -> dimmest)
|
||||
export const text = {
|
||||
primary: '#fafafa',
|
||||
secondary: '#d4d4d8',
|
||||
tertiary: '#a1a1aa',
|
||||
muted: '#71717a',
|
||||
dim: '#52525b',
|
||||
faint: '#3f3f46',
|
||||
} as const;
|
||||
|
||||
// Borders & dividers
|
||||
export const border = {
|
||||
subtle: 'rgba(255, 255, 255, 0.04)',
|
||||
default: 'rgba(255, 255, 255, 0.06)',
|
||||
medium: 'rgba(255, 255, 255, 0.08)',
|
||||
strong: 'rgba(255, 255, 255, 0.1)',
|
||||
primarySubtle: 'rgba(167, 139, 250, 0.1)',
|
||||
primaryDefault: 'rgba(167, 139, 250, 0.15)',
|
||||
primaryMedium: 'rgba(167, 139, 250, 0.2)',
|
||||
primaryStrong: 'rgba(167, 139, 250, 0.3)',
|
||||
} as const;
|
||||
|
||||
// Interactive surface fills
|
||||
export const fill = {
|
||||
subtle: 'rgba(255, 255, 255, 0.03)',
|
||||
default: 'rgba(255, 255, 255, 0.04)',
|
||||
hover: 'rgba(255, 255, 255, 0.06)',
|
||||
primaryHover: 'rgba(167, 139, 250, 0.06)',
|
||||
primaryActive: 'rgba(167, 139, 250, 0.08)',
|
||||
primaryStrong: 'rgba(167, 139, 250, 0.1)',
|
||||
} as const;
|
||||
|
||||
// Gradient presets
|
||||
export const gradients = {
|
||||
brand: `linear-gradient(135deg, ${primary.dark}, ${secondary.main})`,
|
||||
brandText: `linear-gradient(135deg, ${primary.light} 0%, ${secondary.main} 100%)`,
|
||||
button: `linear-gradient(135deg, ${primary.dark} 0%, ${primary.main} 100%)`,
|
||||
buttonHover: `linear-gradient(135deg, ${primary.darker} 0%, ${primary.mid} 100%)`,
|
||||
titleText: `linear-gradient(135deg, ${text.primary} 0%, ${text.tertiary} 100%)`,
|
||||
} as const;
|
||||
|
||||
// Glow / shadow presets
|
||||
export const glow = {
|
||||
brand: '0 0 12px rgba(124, 58, 237, 0.5)',
|
||||
brandStrong: '0 0 20px rgba(124, 58, 237, 0.8), 0 0 40px rgba(34, 211, 238, 0.2)',
|
||||
button: '0 0 20px rgba(124, 58, 237, 0.3)',
|
||||
palette: '0 24px 80px rgba(0, 0, 0, 0.5), 0 0 60px rgba(124, 58, 237, 0.1)',
|
||||
} as const;
|
||||
|
||||
// Game colors
|
||||
export const gameColors: Record<number, string> = {
|
||||
1: '#a78bfa', // Bhop - purple
|
||||
2: '#22d3ee', // Surf - cyan
|
||||
5: '#fbbf24', // Fly Trials - yellow
|
||||
} as const;
|
||||
@@ -1,263 +1,133 @@
|
||||
import { createTheme } from '@mui/material';
|
||||
import { primary, secondary, semantic, surface, text, border, fill, gradients, glow } from './colors';
|
||||
import {createTheme} from "@mui/material";
|
||||
|
||||
export const theme = createTheme({
|
||||
cssVariables: {
|
||||
colorSchemeSelector: 'class',
|
||||
},
|
||||
colorSchemes: {
|
||||
dark: true,
|
||||
},
|
||||
defaultColorScheme: 'dark',
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: primary.main,
|
||||
light: primary.light,
|
||||
dark: primary.dark,
|
||||
main: '#3b82f6',
|
||||
dark: '#2563eb',
|
||||
light: '#60a5fa',
|
||||
},
|
||||
secondary: {
|
||||
main: secondary.main,
|
||||
light: secondary.light,
|
||||
dark: secondary.dark,
|
||||
main: '#8b5cf6',
|
||||
dark: '#7c3aed',
|
||||
light: '#a78bfa',
|
||||
},
|
||||
background: {
|
||||
default: surface.base,
|
||||
paper: surface.raised,
|
||||
default: '#0a0a0a',
|
||||
paper: '#171717',
|
||||
},
|
||||
error: { main: semantic.error },
|
||||
warning: { main: semantic.warning },
|
||||
success: { main: semantic.success },
|
||||
info: { main: semantic.info },
|
||||
text: {
|
||||
primary: text.primary,
|
||||
secondary: text.tertiary,
|
||||
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: '"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: 600,
|
||||
},
|
||||
h6: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
subtitle1: {
|
||||
fontWeight: 500,
|
||||
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',
|
||||
},
|
||||
divider: border.default,
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 12,
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||
fontSize: 14,
|
||||
h1: { fontWeight: 700, letterSpacing: '-0.025em' },
|
||||
h2: { fontWeight: 700, letterSpacing: '-0.02em' },
|
||||
h3: { fontWeight: 600, letterSpacing: '-0.015em' },
|
||||
h4: { fontWeight: 700, letterSpacing: '-0.02em' },
|
||||
h5: { fontWeight: 700, letterSpacing: '-0.02em' },
|
||||
h6: { fontWeight: 700, letterSpacing: '-0.01em' },
|
||||
body1: { fontSize: '1rem', lineHeight: 1.7 },
|
||||
body2: { fontSize: '0.875rem', lineHeight: 1.6 },
|
||||
button: { fontWeight: 600, textTransform: 'none' as const },
|
||||
},
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
body: {
|
||||
backgroundColor: surface.base,
|
||||
backgroundImage: 'radial-gradient(ellipse 80% 50% at 50% -20%, rgba(120, 60, 255, 0.15), transparent)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: surface.appBar,
|
||||
backdropFilter: 'blur(16px)',
|
||||
boxShadow: 'none',
|
||||
borderBottom: `1px solid ${border.default}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
backgroundColor: surface.raisedAlpha,
|
||||
backdropFilter: 'blur(12px)',
|
||||
border: `1px solid ${border.default}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: surface.raisedAlpha,
|
||||
backdropFilter: 'blur(12px)',
|
||||
border: `1px solid ${border.default}`,
|
||||
boxShadow: 'none',
|
||||
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)',
|
||||
border: `1px solid ${border.primaryMedium}`,
|
||||
boxShadow: glow.button,
|
||||
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)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardMedia: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: 'transform 0.3s',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardContent: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: 16,
|
||||
'&:last-child': { paddingBottom: 16 },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableContainer: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
borderRadius: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableHead: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiTableCell-head': {
|
||||
backgroundColor: fill.subtle,
|
||||
color: text.tertiary,
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
textTransform: 'uppercase' as const,
|
||||
letterSpacing: '0.05em',
|
||||
borderBottom: `1px solid ${border.default}`,
|
||||
padding: '12px 16px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableBody: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiTableRow-root': {
|
||||
transition: 'background-color 0.15s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryHover,
|
||||
},
|
||||
},
|
||||
'& .MuiTableCell-body': {
|
||||
borderBottom: `1px solid ${border.subtle}`,
|
||||
padding: '14px 16px',
|
||||
fontSize: '0.875rem',
|
||||
color: text.secondary,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none' as const,
|
||||
fontWeight: 500,
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 44,
|
||||
'&.Mui-selected': { color: primary.main },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTabs: {
|
||||
styleOverrides: {
|
||||
indicator: {
|
||||
backgroundColor: primary.main,
|
||||
height: 2,
|
||||
borderRadius: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none' as const,
|
||||
fontWeight: 600,
|
||||
borderRadius: 8,
|
||||
},
|
||||
contained: {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
containedPrimary: {
|
||||
background: gradients.button,
|
||||
'&:hover': {
|
||||
boxShadow: glow.button,
|
||||
background: gradients.buttonHover,
|
||||
},
|
||||
},
|
||||
containedSuccess: {
|
||||
backgroundColor: semantic.success,
|
||||
color: '#000',
|
||||
'&:hover': {
|
||||
backgroundColor: '#22c55e',
|
||||
boxShadow: '0 0 16px rgba(74, 222, 128, 0.3)',
|
||||
},
|
||||
},
|
||||
containedError: {
|
||||
backgroundColor: semantic.error,
|
||||
'&:hover': {
|
||||
backgroundColor: '#ef4444',
|
||||
boxShadow: '0 0 16px rgba(248, 113, 113, 0.3)',
|
||||
},
|
||||
},
|
||||
containedWarning: {
|
||||
backgroundColor: semantic.warning,
|
||||
color: '#000',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f59e0b',
|
||||
boxShadow: '0 0 16px rgba(251, 191, 36, 0.3)',
|
||||
},
|
||||
},
|
||||
containedInfo: {
|
||||
backgroundColor: semantic.info,
|
||||
'&:hover': {
|
||||
backgroundColor: '#0ea5e9',
|
||||
boxShadow: '0 0 16px rgba(56, 189, 248, 0.3)',
|
||||
},
|
||||
},
|
||||
outlined: {
|
||||
borderColor: border.primaryStrong,
|
||||
color: primary.main,
|
||||
'&:hover': {
|
||||
borderColor: primary.main,
|
||||
backgroundColor: fill.primaryActive,
|
||||
},
|
||||
},
|
||||
outlinedSuccess: {
|
||||
borderColor: `rgba(74, 222, 128, 0.4)`,
|
||||
color: semantic.success,
|
||||
'&:hover': {
|
||||
borderColor: semantic.success,
|
||||
backgroundColor: 'rgba(74, 222, 128, 0.08)',
|
||||
},
|
||||
},
|
||||
outlinedError: {
|
||||
borderColor: `rgba(248, 113, 113, 0.4)`,
|
||||
color: semantic.error,
|
||||
'&:hover': {
|
||||
borderColor: semantic.error,
|
||||
backgroundColor: 'rgba(248, 113, 113, 0.08)',
|
||||
},
|
||||
},
|
||||
outlinedWarning: {
|
||||
borderColor: `rgba(251, 191, 36, 0.4)`,
|
||||
color: semantic.warning,
|
||||
'&:hover': {
|
||||
borderColor: semantic.warning,
|
||||
backgroundColor: 'rgba(251, 191, 36, 0.08)',
|
||||
},
|
||||
},
|
||||
outlinedInfo: {
|
||||
borderColor: `rgba(56, 189, 248, 0.4)`,
|
||||
color: semantic.info,
|
||||
'&:hover': {
|
||||
borderColor: semantic.info,
|
||||
backgroundColor: 'rgba(56, 189, 248, 0.08)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 8,
|
||||
'& fieldset': { borderColor: border.strong },
|
||||
'&:hover fieldset': { borderColor: border.primaryStrong },
|
||||
'&.Mui-focused fieldset': { borderColor: primary.main },
|
||||
'&:last-child': {
|
||||
paddingBottom: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -266,39 +136,134 @@ export const theme = createTheme({
|
||||
styleOverrides: {
|
||||
root: {
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
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: border.default,
|
||||
borderColor: 'rgba(148, 163, 184, 0.1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
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: surface.base,
|
||||
borderRight: `1px solid ${border.default}`,
|
||||
backgroundColor: '#0a0a0a',
|
||||
borderRight: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCircularProgress: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: primary.main,
|
||||
color: '#3b82f6',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: 'all 0.15s ease',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryActive,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -306,11 +271,11 @@ export const theme = createTheme({
|
||||
MuiLink: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: primary.light,
|
||||
color: '#60a5fa',
|
||||
textDecoration: 'none',
|
||||
transition: 'color 0.15s ease',
|
||||
transition: 'color 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
color: primary.main,
|
||||
color: '#3b82f6',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
},
|
||||
@@ -319,28 +284,28 @@ export const theme = createTheme({
|
||||
MuiMenu: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
background: surface.raisedSolid,
|
||||
backdropFilter: 'blur(16px)',
|
||||
border: `1px solid ${border.default}`,
|
||||
boxShadow: glow.palette,
|
||||
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.15s ease',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryHover,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: fill.primaryActive,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.15)',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryStrong,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.2)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -391,12 +391,12 @@ export default function MapDetails() {
|
||||
px: 2,
|
||||
borderRadius: 1,
|
||||
transition: 'all 0.2s',
|
||||
backgroundColor: 'rgba(167, 139, 250, 0.08)',
|
||||
backgroundColor: 'rgba(25, 118, 210, 0.08)',
|
||||
borderLeft: '4px solid',
|
||||
borderColor: 'primary.main',
|
||||
mb: releasedFixes.length > 0 ? 2 : 0,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(167, 139, 250, 0.12)',
|
||||
backgroundColor: 'rgba(25, 118, 210, 0.12)',
|
||||
transform: 'translateX(4px)'
|
||||
},
|
||||
textDecoration: 'none',
|
||||
|
||||
@@ -4,7 +4,6 @@ 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";
|
||||
import { semantic, surface } from "@/app/lib/colors";
|
||||
|
||||
export default function NotFound() {
|
||||
useTitle("404 - Page Not Found");
|
||||
@@ -20,7 +19,7 @@ export default function NotFound() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
background: `linear-gradient(to bottom, ${surface.base} 0%, #0f0f0f 100%)`,
|
||||
background: 'linear-gradient(to bottom, #0a0a0a 0%, #0f0f0f 100%)',
|
||||
}}
|
||||
>
|
||||
{/* Subtle Gradient Background */}
|
||||
@@ -31,7 +30,7 @@ export default function NotFound() {
|
||||
right: '30%',
|
||||
width: '500px',
|
||||
height: '500px',
|
||||
background: `radial-gradient(circle, rgba(248, 113, 113, 0.1) 0%, transparent 70%)`,
|
||||
background: 'radial-gradient(circle, rgba(239, 68, 68, 0.1) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
filter: 'blur(80px)',
|
||||
}}
|
||||
@@ -43,7 +42,7 @@ export default function NotFound() {
|
||||
left: '25%',
|
||||
width: '450px',
|
||||
height: '450px',
|
||||
background: `radial-gradient(circle, rgba(167, 139, 250, 0.08) 0%, transparent 70%)`,
|
||||
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.08) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
filter: 'blur(80px)',
|
||||
}}
|
||||
@@ -60,7 +59,7 @@ export default function NotFound() {
|
||||
lineHeight: 1,
|
||||
mb: 2,
|
||||
letterSpacing: '-0.04em',
|
||||
background: `linear-gradient(135deg, ${semantic.error} 0%, #dc2626 100%)`,
|
||||
background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
@@ -174,7 +173,7 @@ export default function NotFound() {
|
||||
fontSize: '1rem',
|
||||
'&:hover': {
|
||||
color: 'primary.main',
|
||||
background: `rgba(167, 139, 250, 0.1)`,
|
||||
background: 'rgba(59, 130, 246, 0.1)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -26,7 +26,6 @@ import RocketLaunchIcon from "@mui/icons-material/RocketLaunch";
|
||||
import EmojiEventsIcon from "@mui/icons-material/EmojiEvents";
|
||||
import { useUser } from "@/app/hooks/useUser";
|
||||
import { hasAnyReviewerRole } from "@/app/ts/Roles";
|
||||
import { primary, secondary, semantic, text, border, fill, gradients, glow } from "@/app/lib/colors";
|
||||
|
||||
export default function Home() {
|
||||
useTitle("Home");
|
||||
@@ -168,8 +167,8 @@ export default function Home() {
|
||||
}}
|
||||
>
|
||||
<Box display="flex" flexDirection="column" alignItems="center" gap={2}>
|
||||
<CircularProgress size={48} thickness={3} />
|
||||
<Typography variant="body2" sx={{ color: text.muted }}>
|
||||
<CircularProgress size={60} thickness={4} sx={{ color: 'primary.main' }}/>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Loading content...
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -224,32 +223,32 @@ export default function Home() {
|
||||
value: totalSubmissions,
|
||||
label: 'Total Submissions',
|
||||
sublabel: 'Total maps submitted by the community',
|
||||
color: primary.main,
|
||||
gradient: gradients.button,
|
||||
color: '#3b82f6',
|
||||
gradient: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
|
||||
},
|
||||
{
|
||||
icon: <BuildIcon sx={{ fontSize: { xs: 48, md: 64 } }} />,
|
||||
value: totalMapfixes,
|
||||
label: 'Total Map Fixes',
|
||||
sublabel: 'Total map fixes submitted by the community',
|
||||
color: secondary.main,
|
||||
gradient: `linear-gradient(135deg, ${secondary.dark} 0%, ${secondary.main} 100%)`,
|
||||
color: '#8b5cf6',
|
||||
gradient: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)',
|
||||
},
|
||||
{
|
||||
icon: <EmojiEventsIcon sx={{ fontSize: { xs: 48, md: 64 } }} />,
|
||||
value: releasedSubmissions + releasedMapfixes,
|
||||
label: 'Total Released',
|
||||
sublabel: 'Maps & fixes that have been released to the game',
|
||||
color: semantic.success,
|
||||
gradient: `linear-gradient(135deg, ${semantic.success} 0%, #059669 100%)`,
|
||||
color: '#10b981',
|
||||
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
},
|
||||
{
|
||||
icon: <EmojiEventsIcon sx={{ fontSize: { xs: 48, md: 64 } }} />,
|
||||
value: releasedSubmissions,
|
||||
label: 'Released Submissions',
|
||||
sublabel: 'Approved maps that have been published to the game',
|
||||
color: semantic.success,
|
||||
gradient: `linear-gradient(135deg, ${semantic.success} 0%, #059669 100%)`,
|
||||
color: '#10b981',
|
||||
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
},
|
||||
{
|
||||
icon: <EmojiEventsIcon sx={{ fontSize: { xs: 48, md: 64 } }} />,
|
||||
@@ -264,8 +263,8 @@ export default function Home() {
|
||||
value: submittedSubmissions + submittedMapfixes,
|
||||
label: 'Under Review',
|
||||
sublabel: 'Pending approval fixes & submissions',
|
||||
color: semantic.warning,
|
||||
gradient: `linear-gradient(135deg, ${semantic.warning} 0%, #d97706 100%)`,
|
||||
color: '#f59e0b',
|
||||
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -285,8 +284,8 @@ export default function Home() {
|
||||
}}
|
||||
>
|
||||
<Box display="flex" flexDirection="column" alignItems="center" gap={2}>
|
||||
<CircularProgress size={48} thickness={3} />
|
||||
<Typography variant="body2" sx={{ color: text.muted }}>
|
||||
<CircularProgress size={60} thickness={4} sx={{ color: 'primary.main' }}/>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Loading...
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -306,7 +305,7 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<Webpage>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box sx={{ width: '100%', bgcolor: 'background.default' }}>
|
||||
{/* Hero Section */}
|
||||
<Box
|
||||
sx={{
|
||||
@@ -315,9 +314,10 @@ export default function Home() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
background: 'radial-gradient(ellipse at top, #0f1419 0%, #0a0a0a 50%, #000000 100%)',
|
||||
}}
|
||||
>
|
||||
{/* Animated Background Orbs */}
|
||||
{/* Animated Background Elements */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
@@ -325,7 +325,7 @@ export default function Home() {
|
||||
right: '15%',
|
||||
width: { xs: '400px', md: '600px' },
|
||||
height: { xs: '400px', md: '600px' },
|
||||
background: `radial-gradient(circle, rgba(124, 58, 237, 0.12) 0%, transparent 70%)`,
|
||||
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.15) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
filter: 'blur(80px)',
|
||||
animation: 'float 25s ease-in-out infinite',
|
||||
@@ -342,11 +342,11 @@ export default function Home() {
|
||||
left: '10%',
|
||||
width: { xs: '350px', md: '500px' },
|
||||
height: { xs: '350px', md: '500px' },
|
||||
background: `radial-gradient(circle, rgba(34, 211, 238, 0.08) 0%, transparent 70%)`,
|
||||
background: 'radial-gradient(circle, rgba(139, 92, 246, 0.12) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
filter: 'blur(80px)',
|
||||
animation: 'floatReverse 30s ease-in-out infinite',
|
||||
'@keyframes floatReverse': {
|
||||
animation: 'float-reverse 30s ease-in-out infinite',
|
||||
'@keyframes float-reverse': {
|
||||
'0%, 100%': { transform: 'translate(0, 0) scale(1)' },
|
||||
'50%': { transform: 'translate(-30px, 30px) scale(1.15)' },
|
||||
},
|
||||
@@ -359,8 +359,8 @@ export default function Home() {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(124, 58, 237, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(124, 58, 237, 0.03) 1px, transparent 1px)
|
||||
linear-gradient(rgba(59, 130, 246, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(59, 130, 246, 0.03) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '60px 60px',
|
||||
maskImage: 'radial-gradient(ellipse at center, black 20%, transparent 70%)',
|
||||
@@ -376,7 +376,7 @@ export default function Home() {
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '1px',
|
||||
background: `linear-gradient(90deg, transparent 0%, rgba(124, 58, 237, 0.3) 50%, transparent 100%)`,
|
||||
background: 'linear-gradient(90deg, transparent 0%, rgba(59, 130, 246, 0.3) 50%, transparent 100%)',
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
@@ -387,7 +387,7 @@ export default function Home() {
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '1px',
|
||||
background: `linear-gradient(90deg, transparent 0%, rgba(34, 211, 238, 0.2) 50%, transparent 100%)`,
|
||||
background: 'linear-gradient(90deg, transparent 0%, rgba(139, 92, 246, 0.3) 50%, transparent 100%)',
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
@@ -412,7 +412,7 @@ export default function Home() {
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.2em',
|
||||
textTransform: 'uppercase',
|
||||
color: primary.main,
|
||||
color: 'primary.main',
|
||||
mb: 3,
|
||||
display: 'block',
|
||||
opacity: 0.9,
|
||||
@@ -424,16 +424,16 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="h1"
|
||||
sx={{
|
||||
fontFamily: '"Barlow", sans-serif',
|
||||
fontSize: { xs: '3.5rem', sm: '5rem', md: '7rem', lg: '8rem' },
|
||||
fontWeight: 900,
|
||||
lineHeight: 0.95,
|
||||
mb: 2,
|
||||
letterSpacing: '-0.04em',
|
||||
background: gradients.brandText,
|
||||
background: 'linear-gradient(135deg, #60a5fa 0%, #a78bfa 50%, #c084fc 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
textShadow: '0 0 80px rgba(59, 130, 246, 0.3)',
|
||||
}}
|
||||
>
|
||||
StrafesNET
|
||||
@@ -442,7 +442,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{
|
||||
color: text.primary,
|
||||
color: 'text.primary',
|
||||
fontSize: { xs: '1.75rem', sm: '2.25rem', md: '3rem' },
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.02em',
|
||||
@@ -456,7 +456,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
color: text.tertiary,
|
||||
color: 'text.secondary',
|
||||
mb: 5,
|
||||
lineHeight: 1.75,
|
||||
fontWeight: 400,
|
||||
@@ -471,7 +471,7 @@ export default function Home() {
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
{/* CTA Buttons - Moved up for better hierarchy */}
|
||||
<Box
|
||||
display="flex"
|
||||
gap={3}
|
||||
@@ -493,10 +493,13 @@ export default function Home() {
|
||||
px: { xs: 4, md: 5 },
|
||||
py: { xs: 1.75, md: 2.25 },
|
||||
fontWeight: 700,
|
||||
boxShadow: glow.button,
|
||||
background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
|
||||
boxShadow: '0 8px 32px rgba(59, 130, 246, 0.4)',
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
boxShadow: glow.brandStrong,
|
||||
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
|
||||
boxShadow: '0 12px 40px rgba(59, 130, 246, 0.6)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
@@ -516,9 +519,14 @@ export default function Home() {
|
||||
py: { xs: 1.75, md: 2.25 },
|
||||
fontWeight: 700,
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(139, 92, 246, 0.5)',
|
||||
color: '#a78bfa',
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
borderWidth: 2,
|
||||
borderColor: '#a78bfa',
|
||||
background: 'rgba(139, 92, 246, 0.1)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
@@ -528,7 +536,7 @@ export default function Home() {
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Stats Section */}
|
||||
{/* Stats Section - Completely Redesigned */}
|
||||
<Box
|
||||
sx={{
|
||||
animation: 'fadeIn 1.1s ease-out 0.4s both',
|
||||
@@ -538,6 +546,7 @@ export default function Home() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Stats Grid */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
@@ -566,13 +575,22 @@ export default function Home() {
|
||||
cursor: 'pointer',
|
||||
background: currentStatIndex === index
|
||||
? `linear-gradient(135deg, ${stat.color}15 0%, ${stat.color}08 100%)`
|
||||
: fill.subtle,
|
||||
: 'rgba(17, 17, 17, 0.4)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: currentStatIndex === index
|
||||
? `1px solid ${stat.color}40`
|
||||
: `1px solid ${border.subtle}`,
|
||||
: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 3,
|
||||
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&::before': currentStatIndex === index ? {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: -1,
|
||||
background: stat.gradient,
|
||||
borderRadius: 3,
|
||||
opacity: 0.1,
|
||||
zIndex: -1,
|
||||
} : {},
|
||||
'&:hover': {
|
||||
transform: 'translateY(-8px) scale(1.02)',
|
||||
background: `linear-gradient(135deg, ${stat.color}20 0%, ${stat.color}10 100%)`,
|
||||
@@ -581,6 +599,7 @@ export default function Home() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Icon */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -604,12 +623,13 @@ export default function Home() {
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* Value */}
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontWeight: 900,
|
||||
fontSize: { xs: '1.75rem', md: '2.25rem' },
|
||||
color: currentStatIndex === index ? stat.color : text.primary,
|
||||
color: currentStatIndex === index ? stat.color : 'text.primary',
|
||||
letterSpacing: '-0.03em',
|
||||
transition: 'color 0.3s',
|
||||
lineHeight: 1,
|
||||
@@ -618,10 +638,11 @@ export default function Home() {
|
||||
{stat.value}
|
||||
</Typography>
|
||||
|
||||
{/* Label */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: currentStatIndex === index ? text.primary : text.tertiary,
|
||||
color: currentStatIndex === index ? 'text.primary' : 'text.secondary',
|
||||
fontSize: { xs: '0.7rem', md: '0.75rem' },
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
@@ -638,6 +659,7 @@ export default function Home() {
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Featured Stat Description */}
|
||||
<Box
|
||||
key={currentStatIndex}
|
||||
sx={{
|
||||
@@ -651,7 +673,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: text.muted,
|
||||
color: 'text.secondary',
|
||||
fontSize: { xs: '0.9rem', md: '1rem' },
|
||||
fontWeight: 500,
|
||||
maxWidth: '600px',
|
||||
@@ -686,7 +708,8 @@ export default function Home() {
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: text.muted, maxWidth: '600px' }}
|
||||
color="text.secondary"
|
||||
sx={{ maxWidth: '600px' }}
|
||||
>
|
||||
Discover the newest custom maps created by the community
|
||||
</Typography>
|
||||
@@ -717,7 +740,8 @@ export default function Home() {
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: text.muted, maxWidth: '600px' }}
|
||||
color="text.secondary"
|
||||
sx={{ maxWidth: '600px' }}
|
||||
>
|
||||
Community-created map fixes and improvements
|
||||
</Typography>
|
||||
@@ -747,7 +771,8 @@ export default function Home() {
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: text.muted, maxWidth: '600px' }}
|
||||
color="text.secondary"
|
||||
sx={{ maxWidth: '600px' }}
|
||||
>
|
||||
Join the community and start contributing today
|
||||
</Typography>
|
||||
@@ -766,21 +791,21 @@ export default function Home() {
|
||||
title: 'Submit Maps',
|
||||
description: 'Upload your custom bhop and surf maps for review. Maps are evaluated by moderators before being added to the game.',
|
||||
link: '/submit',
|
||||
color: primary.main,
|
||||
color: '#3b82f6',
|
||||
},
|
||||
{
|
||||
icon: <BuildIcon sx={{ fontSize: 48 }} />,
|
||||
title: 'Submit Fixes',
|
||||
description: 'Found bugs or issues in existing maps? Submit fixed versions to improve map quality for all players.',
|
||||
link: '/mapfixes',
|
||||
color: secondary.main,
|
||||
color: '#8b5cf6',
|
||||
},
|
||||
{
|
||||
icon: <ListIcon sx={{ fontSize: 48 }} />,
|
||||
title: 'View Submissions',
|
||||
description: 'Browse all pending and approved submissions currently in the review queue. Track submission status and feedback.',
|
||||
link: '/submissions',
|
||||
color: semantic.success,
|
||||
color: '#10b981',
|
||||
},
|
||||
].map((card, index) => (
|
||||
<Box
|
||||
@@ -789,17 +814,16 @@ export default function Home() {
|
||||
to={card.link}
|
||||
sx={{
|
||||
p: 5,
|
||||
background: fill.subtle,
|
||||
backdropFilter: 'blur(12px)',
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${border.default}`,
|
||||
background: 'rgba(23, 23, 23, 0.5)',
|
||||
borderRadius: 2,
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px)',
|
||||
borderColor: `${card.color}40`,
|
||||
boxShadow: `0 8px 24px rgba(0, 0, 0, 0.4)`,
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4)',
|
||||
'& .icon-box': {
|
||||
background: `${card.color}30`,
|
||||
},
|
||||
@@ -811,8 +835,8 @@ export default function Home() {
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
background: `${card.color}15`,
|
||||
borderRadius: 1.5,
|
||||
background: `${card.color}20`,
|
||||
mb: 3,
|
||||
color: card.color,
|
||||
transition: 'background 0.3s',
|
||||
@@ -823,7 +847,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
fontWeight: 600,
|
||||
mb: 1.5,
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
@@ -833,7 +857,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: text.muted,
|
||||
color: 'text.secondary',
|
||||
lineHeight: 1.7,
|
||||
}}
|
||||
>
|
||||
@@ -850,7 +874,8 @@ export default function Home() {
|
||||
sx={{
|
||||
position: 'relative',
|
||||
py: 12,
|
||||
borderTop: `1px solid ${border.default}`,
|
||||
background: '#0f0f0f',
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md" sx={{ position: 'relative', textAlign: 'center' }}>
|
||||
@@ -868,7 +893,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: text.muted,
|
||||
color: 'text.secondary',
|
||||
mb: 5,
|
||||
lineHeight: 1.7,
|
||||
fontSize: '1.125rem',
|
||||
|
||||
@@ -522,10 +522,7 @@ export default function ReviewerDashboardPage() {
|
||||
mb: 4
|
||||
}}>
|
||||
{canReviewSubmissions && (
|
||||
<Card
|
||||
onClick={() => setTabValue(tabIndexSubmissions)}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Card onClick={()=>setTabValue(tabIndexSubmissions)}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<AssignmentIcon sx={{ fontSize: 40, color: 'primary.main' }} />
|
||||
@@ -549,10 +546,7 @@ export default function ReviewerDashboardPage() {
|
||||
)}
|
||||
|
||||
{canReviewMapfixes && (
|
||||
<Card
|
||||
onClick={() => setTabValue(tabIndexMapfixes)}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Card onClick={()=>setTabValue(tabIndexMapfixes)}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<BuildIcon sx={{ fontSize: 40, color: 'secondary.main' }} />
|
||||
@@ -579,7 +573,15 @@ export default function ReviewerDashboardPage() {
|
||||
<Card
|
||||
component={Link}
|
||||
to="/script-review"
|
||||
sx={{ cursor: 'pointer' }}
|
||||
sx={{
|
||||
textDecoration: 'none',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: 4
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
|
||||
@@ -34,7 +34,6 @@ import { Script } from "@/app/ts/Script";
|
||||
import { useTitle } from "@/app/hooks/useTitle";
|
||||
import { useUser } from "@/app/hooks/useUser";
|
||||
import { RolesConstants, hasRole } from "@/app/ts/Roles";
|
||||
import { primary, semantic, surface, text, border } from "@/app/lib/colors";
|
||||
|
||||
interface SnackbarState {
|
||||
open: boolean;
|
||||
@@ -62,43 +61,43 @@ const IDEButton = ({
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return {
|
||||
bg: primary.dark,
|
||||
hoverBg: primary.mid,
|
||||
activeBg: primary.main,
|
||||
bg: '#0e639c',
|
||||
hoverBg: '#1177bb',
|
||||
activeBg: '#007acc',
|
||||
color: '#ffffff',
|
||||
border: primary.main,
|
||||
border: '#007acc',
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
bg: '#166534',
|
||||
hoverBg: '#15803d',
|
||||
activeBg: semantic.success,
|
||||
bg: '#0e7e0e',
|
||||
hoverBg: '#0f9d0f',
|
||||
activeBg: '#14b814',
|
||||
color: '#ffffff',
|
||||
border: semantic.success,
|
||||
border: '#14b814',
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
bg: '#7f1d1d',
|
||||
hoverBg: '#991b1b',
|
||||
activeBg: semantic.error,
|
||||
bg: '#7e0e0e',
|
||||
hoverBg: '#9d0f0f',
|
||||
activeBg: '#b81414',
|
||||
color: '#ffffff',
|
||||
border: semantic.error,
|
||||
border: '#b81414',
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
bg: '#78350f',
|
||||
hoverBg: '#92400e',
|
||||
activeBg: semantic.warning,
|
||||
bg: '#7e5e0e',
|
||||
hoverBg: '#9d750f',
|
||||
activeBg: '#b88614',
|
||||
color: '#ffffff',
|
||||
border: semantic.warning,
|
||||
border: '#b88614',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
bg: 'transparent',
|
||||
hoverBg: 'rgba(255, 255, 255, 0.08)',
|
||||
activeBg: 'rgba(255, 255, 255, 0.12)',
|
||||
color: text.secondary,
|
||||
border: text.dim,
|
||||
color: '#cccccc',
|
||||
border: '#3e3e42',
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -164,13 +163,13 @@ const InfoBadge = ({
|
||||
const getColors = () => {
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return { bg: `rgba(251, 191, 36, 0.15)`, border: semantic.warning, color: semantic.warning };
|
||||
return { bg: 'rgba(250, 200, 90, 0.15)', border: '#fac85a', color: '#fac85a' };
|
||||
case 'error':
|
||||
return { bg: `rgba(248, 113, 113, 0.15)`, border: semantic.error, color: semantic.error };
|
||||
return { bg: 'rgba(240, 82, 82, 0.15)', border: '#f05252', color: '#f05252' };
|
||||
case 'success':
|
||||
return { bg: `rgba(74, 222, 128, 0.15)`, border: semantic.success, color: semantic.success };
|
||||
return { bg: 'rgba(80, 200, 120, 0.15)', border: '#50c878', color: '#50c878' };
|
||||
default:
|
||||
return { bg: `rgba(167, 139, 250, 0.15)`, border: primary.main, color: primary.main };
|
||||
return { bg: 'rgba(100, 150, 230, 0.15)', border: '#6496e6', color: '#6496e6' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -637,15 +636,15 @@ export default function ScriptReviewPage() {
|
||||
<Box sx={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
bgcolor: surface.raised,
|
||||
bgcolor: '#1e1e1e',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: text.secondary,
|
||||
color: '#cccccc',
|
||||
}}>
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<LinearProgress sx={{ mb: 2, width: 300 }} />
|
||||
<Typography sx={{ color: text.secondary, fontSize: '14px' }}>Loading script...</Typography>
|
||||
<Typography sx={{ color: '#cccccc', fontSize: '14px' }}>Loading script...</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -704,13 +703,13 @@ export default function ScriptReviewPage() {
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
bgcolor: surface.raised,
|
||||
bgcolor: '#1e1e1e',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{/* Title Bar */}
|
||||
<Box sx={{
|
||||
bgcolor: '#27272a',
|
||||
borderBottom: `1px solid ${border.default}`,
|
||||
bgcolor: '#323233',
|
||||
borderBottom: '1px solid #2b2b2c',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
px: 2,
|
||||
@@ -718,8 +717,8 @@ export default function ScriptReviewPage() {
|
||||
gap: 2,
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<CodeIcon sx={{ fontSize: 20, color: primary.dark }} />
|
||||
<Typography sx={{ fontSize: '13px', fontWeight: 600, color: text.secondary, fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }}>
|
||||
<CodeIcon sx={{ fontSize: 20, color: '#007acc' }} />
|
||||
<Typography sx={{ fontSize: '13px', fontWeight: 600, color: '#cccccc', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }}>
|
||||
Script Review
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -733,7 +732,7 @@ export default function ScriptReviewPage() {
|
||||
>
|
||||
Previous
|
||||
</IDEButton>
|
||||
<Typography sx={{ fontSize: '12px', color: text.muted, px: 1 }}>
|
||||
<Typography sx={{ fontSize: '12px', color: '#858585', px: 1 }}>
|
||||
{currentIndex + 1} / {allScripts.length}
|
||||
</Typography>
|
||||
<IDEButton
|
||||
@@ -753,12 +752,12 @@ export default function ScriptReviewPage() {
|
||||
gap: 1,
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
bgcolor: `rgba(251, 191, 36, 0.15)`,
|
||||
bgcolor: 'rgba(250, 200, 90, 0.15)',
|
||||
borderRadius: '2px',
|
||||
border: `1px solid ${semantic.warning}`,
|
||||
border: '1px solid #fac85a',
|
||||
}}>
|
||||
<WarningAmberIcon sx={{ fontSize: '14px', color: semantic.warning }} />
|
||||
<Typography sx={{ fontSize: '11px', color: semantic.warning, fontWeight: 500 }}>
|
||||
<WarningAmberIcon sx={{ fontSize: '14px', color: '#fac85a' }} />
|
||||
<Typography sx={{ fontSize: '11px', color: '#fac85a', fontWeight: 500 }}>
|
||||
UNSAVED CHANGES
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -788,16 +787,16 @@ export default function ScriptReviewPage() {
|
||||
width: 300,
|
||||
minWidth: 300,
|
||||
flexShrink: 0,
|
||||
bgcolor: surface.raised,
|
||||
borderRight: `1px solid ${border.default}`,
|
||||
bgcolor: '#252526',
|
||||
borderRight: '1px solid #2b2b2c',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
{/* Script Info Section */}
|
||||
<Box sx={{ p: 2, borderBottom: `1px solid ${border.default}` }}>
|
||||
<Box sx={{ p: 2, borderBottom: '1px solid #2b2b2c' }}>
|
||||
<Typography sx={{
|
||||
color: text.muted,
|
||||
color: '#858585',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.5px',
|
||||
@@ -807,11 +806,11 @@ export default function ScriptReviewPage() {
|
||||
SCRIPT PROPERTIES
|
||||
</Typography>
|
||||
<Box sx={{ mb: 1.5 }}>
|
||||
<Typography sx={{ color: text.muted, fontSize: '10px', mb: 0.5 }}>
|
||||
<Typography sx={{ color: '#858585', fontSize: '10px', mb: 0.5 }}>
|
||||
Name
|
||||
</Typography>
|
||||
<Typography sx={{
|
||||
color: text.secondary,
|
||||
color: '#d4d4d4',
|
||||
fontFamily: '"Cascadia Code", "Courier New", monospace',
|
||||
fontSize: '12px',
|
||||
wordBreak: 'break-word',
|
||||
@@ -820,11 +819,11 @@ export default function ScriptReviewPage() {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography sx={{ color: text.muted, fontSize: '10px', mb: 0.5 }}>
|
||||
<Typography sx={{ color: '#858585', fontSize: '10px', mb: 0.5 }}>
|
||||
Hash
|
||||
</Typography>
|
||||
<Typography sx={{
|
||||
color: text.muted,
|
||||
color: '#858585',
|
||||
fontFamily: '"Cascadia Code", "Courier New", monospace',
|
||||
fontSize: '10px',
|
||||
wordBreak: 'break-all',
|
||||
@@ -835,9 +834,9 @@ export default function ScriptReviewPage() {
|
||||
</Box>
|
||||
|
||||
{/* Policy Selection Section */}
|
||||
<Box sx={{ p: 2, borderBottom: `1px solid ${border.default}` }}>
|
||||
<Box sx={{ p: 2, borderBottom: '1px solid #2b2b2c' }}>
|
||||
<Typography sx={{
|
||||
color: text.muted,
|
||||
color: '#858585',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.5px',
|
||||
@@ -930,9 +929,9 @@ export default function ScriptReviewPage() {
|
||||
title="Purge Script & Policy"
|
||||
style={{
|
||||
padding: '6px',
|
||||
backgroundColor: submitting || sourceChanged ? text.dim : '#7f1d1d',
|
||||
backgroundColor: submitting || sourceChanged ? '#3e3e42' : '#7e0e0e',
|
||||
color: '#ffffff',
|
||||
border: `1px solid ${submitting || sourceChanged ? text.dim : semantic.error}`,
|
||||
border: `1px solid ${submitting || sourceChanged ? '#3e3e42' : '#b81414'}`,
|
||||
borderRadius: '2px',
|
||||
cursor: submitting || sourceChanged ? 'not-allowed' : 'pointer',
|
||||
opacity: submitting || sourceChanged ? 0.4 : 1,
|
||||
@@ -947,22 +946,22 @@ export default function ScriptReviewPage() {
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!submitting && !sourceChanged) {
|
||||
e.currentTarget.style.backgroundColor = '#991b1b';
|
||||
e.currentTarget.style.backgroundColor = '#9d0f0f';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!submitting && !sourceChanged) {
|
||||
e.currentTarget.style.backgroundColor = '#7f1d1d';
|
||||
e.currentTarget.style.backgroundColor = '#7e0e0e';
|
||||
}
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
if (!submitting && !sourceChanged) {
|
||||
e.currentTarget.style.backgroundColor = semantic.error;
|
||||
e.currentTarget.style.backgroundColor = '#b81414';
|
||||
}
|
||||
}}
|
||||
onMouseUp={(e) => {
|
||||
if (!submitting && !sourceChanged) {
|
||||
e.currentTarget.style.backgroundColor = '#991b1b';
|
||||
e.currentTarget.style.backgroundColor = '#9d0f0f';
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -977,15 +976,15 @@ export default function ScriptReviewPage() {
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Box sx={{
|
||||
p: 1.5,
|
||||
bgcolor: `rgba(248, 113, 113, 0.15)`,
|
||||
border: `1px solid ${semantic.error}`,
|
||||
bgcolor: 'rgba(240, 82, 82, 0.15)',
|
||||
border: '1px solid #f05252',
|
||||
borderRadius: '2px',
|
||||
mb: 1,
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '11px', color: semantic.error, fontWeight: 500, mb: 0.5 }}>
|
||||
<Typography sx={{ fontSize: '11px', color: '#f05252', fontWeight: 500, mb: 0.5 }}>
|
||||
⚠️ Permanent Deletion
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '10px', color: text.secondary, lineHeight: 1.4 }}>
|
||||
<Typography sx={{ fontSize: '10px', color: '#cccccc', lineHeight: 1.4 }}>
|
||||
This will permanently delete the script and policy. This action cannot be undone.
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -1022,21 +1021,21 @@ export default function ScriptReviewPage() {
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Tab Bar */}
|
||||
<Box sx={{
|
||||
bgcolor: '#27272a',
|
||||
borderBottom: `1px solid ${surface.raised}`,
|
||||
bgcolor: '#2d2d2d',
|
||||
borderBottom: '1px solid #1e1e1e',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
minHeight: 35,
|
||||
}}>
|
||||
<Box sx={{
|
||||
bgcolor: surface.raised,
|
||||
bgcolor: '#1e1e1e',
|
||||
px: 2,
|
||||
py: 0.75,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
borderTop: sourceChanged ? `2px solid ${semantic.warning}` : `2px solid ${primary.dark}`,
|
||||
borderTop: sourceChanged ? '2px solid #f59e0b' : '2px solid #007acc',
|
||||
color: '#ffffff',
|
||||
minHeight: 35,
|
||||
}}>
|
||||
@@ -1049,7 +1048,7 @@ export default function ScriptReviewPage() {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: semantic.warning
|
||||
bgcolor: '#f59e0b'
|
||||
}} />
|
||||
)}
|
||||
</Box>
|
||||
@@ -1114,7 +1113,7 @@ export default function ScriptReviewPage() {
|
||||
|
||||
{/* Status Bar */}
|
||||
<Box sx={{
|
||||
bgcolor: primary.dark,
|
||||
bgcolor: '#007acc',
|
||||
color: 'white',
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
@@ -1153,9 +1152,9 @@ export default function ScriptReviewPage() {
|
||||
onClose={handleCloseSnackbar}
|
||||
severity={snackbar.severity}
|
||||
sx={{
|
||||
bgcolor: surface.raised,
|
||||
color: text.secondary,
|
||||
border: `1px solid ${text.faint}`,
|
||||
bgcolor: '#252526',
|
||||
color: '#cccccc',
|
||||
border: '1px solid #3e3e42',
|
||||
}}
|
||||
>
|
||||
{snackbar.message}
|
||||
|
||||
@@ -6,7 +6,6 @@ import Webpage from "@/app/_components/webpage";
|
||||
import { ListSortConstants } from "../ts/Sort";
|
||||
import { hasAnyReviewerRole } from "../ts/Roles";
|
||||
import { useUser } from "@/app/hooks/useUser";
|
||||
import { primary, semantic } from "@/app/lib/colors";
|
||||
import {
|
||||
Box,
|
||||
Breadcrumbs,
|
||||
@@ -408,7 +407,7 @@ export default function UserDashboardPage() {
|
||||
gap: 2,
|
||||
mb: 4
|
||||
}}>
|
||||
<Card sx={{ background: `linear-gradient(135deg, ${primary.main} 0%, ${primary.dark} 100%)` }}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
|
||||
<CardContent>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
|
||||
Total Contributions
|
||||
@@ -423,7 +422,7 @@ export default function UserDashboardPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card sx={{ background: `linear-gradient(135deg, ${semantic.success} 0%, ${primary.mid} 100%)` }}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' }}>
|
||||
<CardContent>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
|
||||
Released
|
||||
@@ -438,7 +437,7 @@ export default function UserDashboardPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card sx={{ background: `linear-gradient(135deg, ${semantic.info} 0%, ${primary.light} 100%)` }}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }}>
|
||||
<CardContent>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
|
||||
In Review
|
||||
@@ -453,7 +452,7 @@ export default function UserDashboardPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card sx={{ background: `linear-gradient(135deg, ${semantic.error} 0%, ${semantic.warning} 100%)` }}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' }}>
|
||||
<CardContent>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
|
||||
Action Needed
|
||||
|
||||
@@ -16,17 +16,17 @@ export function getGameInfo(gameId: number) {
|
||||
case 1:
|
||||
return {
|
||||
name: "Bhop",
|
||||
color: "#a78bfa" // purple
|
||||
color: "#2196f3" // blue
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
name: "Surf",
|
||||
color: "#22d3ee" // cyan
|
||||
color: "#4caf50" // green
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
name: "Fly Trials",
|
||||
color: "#fbbf24" // yellow
|
||||
color: "#ff9800" // orange
|
||||
};
|
||||
default:
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user