Web: Add Mapfixes Pages #60

Merged
Quaternions merged 4 commits from frontend/mapfixes into staging 2025-04-01 22:27:00 +00:00
23 changed files with 1211 additions and 1 deletions

@ -0,0 +1,75 @@
@forward "./page/card.scss";
@use "../../globals.scss";
a {
color:rgb(255, 255, 255);
&:visited, &:hover, &:focus {
text-decoration: none;
color: rgb(255, 255, 255);
}
&:active {
color: rgb(192, 192, 192)
}
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
grid-template-rows: repeat(3, 1fr);
gap: 16px;
max-width: 100%;
margin: 0 auto;
overflow-x: hidden;
box-sizing: border-box;
}
@media (max-width: 768px) {
.grid {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin: 0.3rem;
}
.pagination button {
padding: 0.25rem 0.5rem;
font-size: 1.15rem;
border: none;
border-radius: 0.35rem;
background-color: #33333350;
color: #fff;
cursor: pointer;
}
.pagination button:disabled {
background-color: #5555559a;
cursor: not-allowed;
}
.pagination-dots {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
justify-content: center;
width: 100%;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #bbb;
cursor: pointer;
}
.dot.active {
background-color: #333;
}

@ -0,0 +1,87 @@
@use "../../../globals.scss";
.submissionCard {
display: flex;
background-color: #2020207c;
border: 1px solid #97979783;
border-radius: 10px;
box-sizing: border-box;
flex-direction: column;
justify-content: space-between;
height: 100%;
min-width: 180px;
max-width: 340px;
}
.content {
display: flex;
flex-grow: 1;
flex-direction: column;
justify-content: space-between;
}
.details {
padding: 2px 4px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin: 3px 0px;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin: 3px 0px;
}
.map-image {
border-radius: 10px 10px 0 0;
overflow: hidden;
> img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.displayName {
font-size: 1rem;
font-weight: bold;
overflow: hidden;
max-width: 70%;
text-overflow: ellipsis;
white-space: nowrap;
}
.rating {
flex-shrink: 0;
}
.author {
display: flex;
align-items: center;
gap: 0.5rem;
flex-grow: 1;
min-width: 0;
}
.author span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rating {
margin-left: auto;
flex-shrink: 0;
}
.avatar {
border-radius: 50%;
object-fit: cover;
}

@ -0,0 +1,19 @@
@forward "./page/commentWindow.scss";
@forward "./page/reviewStatus.scss";
@forward "./page/ratingWindow.scss";
@forward "./page/reviewButtons.scss";
@forward "./page/comments.scss";
@forward "./page/review.scss";
@forward "./page/map.scss";
@use "../../../globals.scss";
.map-page-main {
display: flex;
justify-content: center;
width: 100vw;
}
.by-creator {
margin-top: 10px;
}

@ -0,0 +1,56 @@
@use "../../../../globals.scss";
#comment-text-field {
@include globals.border-with-radius;
resize: none;
width: 100%;
height: 100px;
background-color: var(--comment-area)
}
.leave-comment-window {
@include globals.border-with-radius;
width: 100%;
height: 230px;
margin-top: 35px;
.rating-type {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
gap: 35%;
.rating-right {
display: grid;
> span {
margin: 6px 0 6px 0;
}
}
p {
margin: 15px 0 15px 0;
}
}
header {
display: flex;
align-items: center;
background-color: var(--window-header);
border-bottom: globals.$review-border;
height: 45px;
p {
font-weight: bold;
margin: 0 0 0 20px;
}
}
main {
padding: 20px;
button {
margin-top: 9px;
}
}
}

@ -0,0 +1,49 @@
$comments-size: 60px;
.comments {
display: grid;
gap: 25px;
margin-top: 20px;
.no-comments {
text-align: center;
margin: 0;
}
.commenter {
display: flex;
height: $comments-size;
//BhopMaptest comment
&[data-highlighted="true"] {
background-color: var(--comment-highlighted);
}
> img {
border-radius: 50%;
}
.name {
font: {
weight: 500;
size: 1.3em;
};
}
.date {
font-size: .8em;
margin: 0 0 0 5px;
color: #646464
}
.details {
display: grid;
margin-left: 10px;
header {
display: flex;
align-items: center;
}
p:not(.date) {
margin: 0;
}
}
}
}

@ -0,0 +1,15 @@
@use "../../../../globals.scss";
.map-image-area {
@include globals.border-with-radius;
display: flex;
justify-content: center;
align-items: center;
width: 350px;
height: 350px;
> p {
text-align: center;
margin: 0;
}
}

@ -0,0 +1,43 @@
@use "../../../../globals.scss";
.rating-window {
@include globals.border-with-radius;
width: 100%;
.rating-type {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
gap: 35%;
.rating-right {
display: grid;
> span {
margin: 6px 0 6px 0;
}
}
p {
margin: 15px 0 15px 0;
}
}
header {
display: flex;
align-items: center;
background-color: var(--window-header);
border-bottom: globals.$review-border;
height: 45px;
p {
font-weight: bold;
margin: 0 0 0 20px;
}
}
main {
display: grid;
place-items: center;
}
}

@ -0,0 +1,47 @@
@use "../../../../globals.scss";
.review-info {
width: 650px;
height: 100%;
> div {
display: flex;
justify-content: space-between;
align-items: center;
}
p, h1 {
color: var(--text-color);
}
h1 {
font: {
weight: 500;
size: 1.8rem
};
margin: 0;
}
a {
color: var(--anchor-link-review);
&:hover {
text-decoration: underline;
}
}
}
.review-section {
display: flex;
gap: 50px;
margin-top: 20px;
}
.review-area {
display: grid;
justify-content: center;
gap: 25px;
img {
width: 100%;
height: 350px;
object-fit: contain
}
}

@ -0,0 +1,13 @@
@use "../../../../globals.scss";
.review-set {
@include globals.border-with-radius;
display: grid;
align-items: center;
gap: 10px;
padding: 10px;
button {
width: 100%;
}
}

@ -0,0 +1,80 @@
$UnderConstruction: "0";
$Submitted: "1";
$ChangesRequested: "2";
$Accepted: "3";
$Validating: "4";
$Validated: "5";
$Uploading: "6";
$Uploaded: "7";
$Rejected: "8";
$Released: "9";
.review-status {
border-radius: 5px;
p {
margin: 3px 25px 3px 25px;
font-weight: bold;
}
&[data-review-status="#{$Released}"] {
background-color: orange;
p {
color: white;
}
}
&[data-review-status="#{$Rejected}"] {
background-color: orange;
p {
color: white;
}
}
&[data-review-status="#{$Uploading}"] {
background-color: orange;
p {
color: white;
}
}
&[data-review-status="#{$Uploaded}"] {
background-color: orange;
p {
color: white;
}
}
&[data-review-status="#{$Validated}"] {
background-color: orange;
p {
color: white;
}
}
&[data-review-status="#{$Validating}"] {
background-color: orange;
p {
color: white;
}
}
&[data-review-status="#{$Accepted}"] {
background-color: rgb(2, 162, 2);
p {
color: white;
}
}
&[data-review-status="#{$ChangesRequested}"] {
background-color: orange;
p {
color: white;
}
}
&[data-review-status="#{$Submitted}"] {
background-color: orange;
p {
color: white;
}
}
&[data-review-status="#{$UnderConstruction}"] {
background-color: orange;
p {
color: white;
}
}
}

@ -0,0 +1,68 @@
import type { MapfixInfo } from "@/app/ts/Mapfix";
import { Button } from "@mui/material"
import Window from "./_window";
import SendIcon from '@mui/icons-material/Send';
import Image from "next/image";
interface CommentersProps {
comments_data: CreatorAndReviewStatus
}
interface CreatorAndReviewStatus {
asset_id: MapfixInfo["AssetID"],
creator: MapfixInfo["DisplayName"],
review: MapfixInfo["StatusID"],
status_message: MapfixInfo["StatusMessage"],
comments: Comment[],
name: string
}
interface Comment {
picture?: string, //TEMP
comment: string,
date: string,
name: string
}
function AddComment(comment: Comment) {
const IsBhopMaptest = comment.name == "BhopMaptest" //Highlighted commenter
return (
<div className="commenter" data-highlighted={IsBhopMaptest}>
<Image src={comment.picture as string} alt={`${comment.name}'s comment`}/>
<div className="details">
<header>
<p className="name">{comment.name}</p>
<p className="date">{comment.date}</p>
</header>
<p className="comment">{comment.comment}</p>
</div>
</div>
);
}
function LeaveAComment() {
return (
<Window title="Leave a Comment:" className="leave-comment-window">
<textarea name="comment-box" id="comment-text-field"></textarea>
<Button variant="outlined" endIcon={<SendIcon/>}>Submit</Button>
</Window>
)
}
export default function Comments(stats: CommentersProps) {
return (<>
<section className="comments">
{stats.comments_data.comments.length===0
&& <p className="no-comments">There are no comments.</p>
|| stats.comments_data.comments.map(comment => (
<AddComment key={comment.name} name={comment.name} date={comment.date} comment={comment.comment}/>
))}
</section>
<LeaveAComment/>
</>)
}
export {
type CreatorAndReviewStatus
}

@ -0,0 +1,14 @@
import { MapfixInfo } from "@/app/ts/Mapfix"
interface AssetID {
id: MapfixInfo["AssetID"]
}
function MapImage() {
return <p>Fetching map image...</p>
}
export {
type AssetID,
MapImage
}

@ -0,0 +1,74 @@
import { Button, ButtonOwnProps } from "@mui/material";
type Actions = "Completed" | "Submit" | "Reject" | "Revoke"
type ApiActions = Lowercase<Actions> | "trigger-validate" | "retry-validate" | "trigger-upload" | "reset-uploading" | "reset-validating"
type Review = Actions | "Accept" | "Validate" | "Upload" | "Reset Uploading (fix softlocked status)" | "Reset Validating (fix softlocked status)" | "Request Changes"
interface ReviewButton {
name: Review,
action: ApiActions,
mapfixId: string,
color: ButtonOwnProps["color"]
}
interface ReviewId {
mapfixId: string
}
async function ReviewButtonClicked(action: ApiActions, mapfixId: string) {
try {
const response = await fetch(`/api/mapfixes/${mapfixId}/status/${action}`, {
method: "POST",
headers: {
"Content-type": "application/json",
}
});
// Check if the HTTP request was successful
if (!response.ok) {
const errorDetails = await response.text();
// Throw an error with detailed information
throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`);
}
window.location.reload();
} catch (error) {
console.error("Error updating mapfix status:", error);
}
}
function ReviewButton(props: ReviewButton) {
return <Button
color={props.color}
variant="contained"
onClick={() => { ReviewButtonClicked(props.action, props.mapfixId) }}>{props.name}</Button>
}
export default function ReviewButtons(props: ReviewId) {
const mapfixId = props.mapfixId
// When is each button visible?
// Multiple buttons can be visible at once.
// Action | Role | When Current Status is One of:
// ---------------|-----------|-----------------------
// Submit | Submitter | UnderConstruction, ChangesRequested
// Revoke | Submitter | Submitted, ChangesRequested
// Accept | Reviewer | Submitted
// Validate | Reviewer | Accepted
// ResetValidating| Reviewer | Validating
// Reject | Reviewer | Submitted
// RequestChanges | Reviewer | Validated, Accepted, Submitted
// Upload | MapAdmin | Validated
// ResetUploading | MapAdmin | Uploading
return (
<section className="review-set">
<ReviewButton color="info" name="Submit" action="submit" mapfixId={mapfixId}/>
<ReviewButton color="info" name="Revoke" action="revoke" mapfixId={mapfixId}/>
<ReviewButton color="info" name="Accept" action="trigger-validate" mapfixId={mapfixId}/>
<ReviewButton color="info" name="Validate" action="retry-validate" mapfixId={mapfixId}/>
<ReviewButton color="error" name="Reject" action="reject" mapfixId={mapfixId}/>
<ReviewButton color="info" name="Upload" action="trigger-upload" mapfixId={mapfixId}/>
<ReviewButton color="error" name="Reset Uploading (fix softlocked status)" action="reset-uploading" mapfixId={mapfixId}/>
<ReviewButton color="error" name="Reset Validating (fix softlocked status)" action="reset-validating" mapfixId={mapfixId}/>
</section>
)
}

@ -0,0 +1,20 @@
interface WindowStruct {
className: string,
title: string,
children: React.ReactNode
}
export default function Window(window: WindowStruct) {
return (
<section className={window.className}>
<header>
<p>{window.title}</p>
</header>
<main>{window.children}</main>
</section>
)
}
export {
type WindowStruct
}

@ -0,0 +1,105 @@
"use client"
import { MapfixInfo, MapfixStatusToString } from "@/app/ts/Mapfix";
import type { CreatorAndReviewStatus } from "./_comments";
import { MapImage } from "./_map";
import { useParams } from "next/navigation";
import ReviewButtons from "./_reviewButtons";
import { Rating } from "@mui/material";
import Comments from "./_comments";
import Webpage from "@/app/_components/webpage";
import Window from "./_window";
import Link from "next/link";
import { useState, useEffect } from "react";
import "./(styles)/page.scss";
interface ReviewId {
mapfixId: string
}
function Ratings() {
return (
<Window className="rating-window" title="Rating">
<section className="rating-type">
<aside className="rating-left">
<p>Quality</p>
<p>Difficulty</p>
<p>Fun</p>
<p>Length</p>
</aside>
<aside className="rating-right">
<Rating defaultValue={2.5} precision={0.5}/>
<Rating defaultValue={2.5} precision={0.5}/>
<Rating defaultValue={2.5} precision={0.5}/>
<Rating defaultValue={2.5} precision={0.5}/>
</aside>
</section>
</Window>
)
}
function RatingArea(mapfix: ReviewId) {
return (
<aside className="review-area">
<section className="map-image-area">
<MapImage/>
</section>
<Ratings/>
<ReviewButtons mapfixId={mapfix.mapfixId}/>
</aside>
)
}
function TitleAndComments(stats: CreatorAndReviewStatus) {
const Review = MapfixStatusToString(stats.review)
// TODO: hide status message when status is not "Accepted"
return (
<main className="review-info">
<div>
<h1>{stats.name}</h1>
<aside data-review-status={stats.review} className="review-status">
<p>{Review}</p>
</aside>
</div>
<p className="by-creator">by <Link href="" target="_blank">{stats.creator}</Link></p>
<p className="asset-id">Model Asset ID {stats.asset_id}</p>
<p className="status-message">Validation Error: {stats.status_message}</p>
<span className="spacer"></span>
<Comments comments_data={stats}/>
</main>
)
}
export default function MapfixInfoPage() {
const dynamicId = useParams<{mapfixId: string}>()
const [mapfix, setMapfix] = useState<MapfixInfo | null>(null)
useEffect(() => { // needs to be client sided since server doesn't have a session, nextjs got mad at me for exporting an async function: (https://nextjs.org/docs/messages/no-async-client-component)
async function getMapfix() {
const res = await fetch(`/api/mapfixes/${dynamicId.mapfixId}`)
if (res.ok) {
setMapfix(await res.json())
}
}
getMapfix()
}, [dynamicId.mapfixId])
if (!mapfix) {
return <Webpage>
{/* TODO: Add skeleton loading thingy ? Maybe ? (https://mui.com/material-ui/react-skeleton/) */}
</Webpage>
}
return (
<Webpage>
<main className="map-page-main">
<section className="review-section">
<RatingArea mapfixId={dynamicId.mapfixId}/>
<TitleAndComments name={mapfix.DisplayName} creator={mapfix.Creator} review={mapfix.StatusID} status_message={mapfix.StatusMessage} asset_id={mapfix.AssetID} comments={[]}/>
</section>
</main>
</Webpage>
)
}

@ -0,0 +1,41 @@
import React from "react";
import Image from "next/image";
import Link from "next/link";
import { Rating } from "@mui/material";
interface SubmissionCardProps {
displayName: string;
assetId: number;
rating: number;
author: string;
id: number;
}
export default function SubmissionCard(props: SubmissionCardProps) {
return (
<Link href={`/submissions/${props.id}`}>
<div className="submissionCard">
<div className="content">
<div className="map-image">
{/* TODO: Grab image of model */}
<Image height={200} width={200} priority={true} src="https://api.ic3.space/strafe/map-images/11222350808" style={{ width: `100%` }} alt={props.displayName} />
</div>
<div className="details">
<div className="header">
<span className="displayName">{props.displayName}</span>
<div className="rating">
<Rating value={props.rating} readOnly size="small" />
</div>
</div>
<div className="footer">
<div className="author">
<Image className="avatar" width={28} height={28} priority={true} src="https://api.ic3.space/strafe/map-images/11222350808" alt={props.author}/>
<span>{props.author}</span>
</div>
</div>
</div>
</div>
</div>
</Link>
);
}

@ -0,0 +1,18 @@
interface WindowStruct {
children: React.ReactNode,
className: string,
title: string,
}
export default function Window(window: WindowStruct) {
return <section className={window.className}>
<header>
<p>{window.title}</p>
</header>
<main>{window.children}</main>
</section>
}
export {
type WindowStruct
}

@ -0,0 +1,110 @@
'use client'
import React, { useState, useEffect } from "react";
import { MapfixInfo } from "../ts/Mapfix";
import MapfixCard from "./_card";
import Webpage from "@/app/_components/webpage";
import "./(styles)/page.scss";
export default function MapfixInfoPage() {
const [mapfixes, setMapfixes] = useState<MapfixInfo[]>([])
const [currentPage, setCurrentPage] = useState(0);
const cardsPerPage = 24; // built to fit on a 1920x1080 monitor
const totalPages = Math.ceil(mapfixes.length / cardsPerPage);
const currentCards = mapfixes.slice(
currentPage * cardsPerPage,
(currentPage + 1) * cardsPerPage
);
const nextPage = () => {
if (currentPage < totalPages - 1) {
setCurrentPage(currentPage + 1);
}
};
const prevPage = () => {
if (currentPage > 0) {
setCurrentPage(currentPage - 1);
}
};
useEffect(() => {
async function fetchMapfixes() {
const res = await fetch('/api/mapfixes?Page=1&Limit=100')
if (res.ok) {
setMapfixes(await res.json())
}
}
setTimeout(() => {
fetchMapfixes()
}, 50);
}, [])
if (!mapfixes) {
return <Webpage>
<main>
Loading...
</main>
</Webpage>
}
if (mapfixes && mapfixes.length == 0) {
return <Webpage>
<main>
Mapfixes list is empty.
</main>
</Webpage>
}
return (
// TODO: Add filter settings & searchbar & page selector
<Webpage>
<main
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
padding: '1rem',
width: '100%',
maxWidth: '100vw',
boxSizing: 'border-box',
overflowX: 'hidden'
}}
>
<div className="pagination-dots">
{Array.from({ length: totalPages }).map((_, index) => (
<span
key={index}
className={`dot ${index === currentPage ? 'active' : ''}`}
onClick={() => setCurrentPage(index)}
></span>
))}
</div>
<div className="pagination">
<button onClick={prevPage} disabled={currentPage === 0}>&lt;</button>
<span>
Page {currentPage + 1} of {totalPages}
</span>
<button onClick={nextPage} disabled={currentPage === totalPages - 1}>&gt;</button>
</div>
<div className="grid">
{currentCards.map((mapfix) => (
<MapfixCard
key={mapfix.ID}
id={mapfix.ID}
assetId={mapfix.AssetID}
displayName={mapfix.DisplayName}
author={mapfix.Creator}
rating={mapfix.StatusID}
/>
))}
</div>
</main>
</Webpage>
)
}

@ -0,0 +1,54 @@
@use "../../../../globals.scss";
::placeholder {
color: var(--placeholder-text)
}
.form-spacer {
margin-bottom: 20px;
&:last-of-type {
margin-top: 15px;
}
}
#target-asset-radio {
color: var(--text-color);
font-size: globals.$form-label-fontsize;
}
.form-field {
width: 850px;
& label, & input {
color: var(--text-color);
}
& fieldset {
border-color: rgb(100,100,100);
}
& span {
color: white;
}
}
main {
display: grid;
justify-content: center;
align-items: center;
margin-inline: auto;
width: 700px;
}
header h1 {
text-align: center;
color: var(--text-color);
}
form {
display: grid;
gap: 25px;
fieldset {
border: blue
}
}

@ -0,0 +1,65 @@
import { FormControl, Select, InputLabel, MenuItem } from "@mui/material";
import { styled } from '@mui/material/styles';
import InputBase from '@mui/material/InputBase';
import React from "react";
import { SelectChangeEvent } from "@mui/material";
// TODO: Properly style everything instead of pasting 🤚
type GameSelectionProps = {
game: number;
setGame: React.Dispatch<React.SetStateAction<number>>;
};
const BootstrapInput = styled(InputBase)(({ theme }) => ({
'label + &': {
marginTop: theme.spacing(3),
},
'& .MuiInputBase-input': {
backgroundColor: '#0000',
color: '#FFF',
border: '1px solid rgba(175, 175, 175, 0.66)',
fontSize: 16,
padding: '10px 26px 10px 12px',
transition: theme.transitions.create(['border-color', 'box-shadow']),
fontFamily: [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
].join(','),
'&:focus': {
borderRadius: 4,
borderColor: '#80bdff',
boxShadow: '0 0 0 0.2rem rgba(0,123,255,.25)',
},
},
}));
export default function GameSelection({ game, setGame }: GameSelectionProps) {
const handleChange = (event: SelectChangeEvent) => {
setGame(Number(event.target.value)); // TODO: Change later!! there's 100% a proper way of doing this
};
return (
<FormControl>
<InputLabel sx={{ color: "#646464" }}>Game</InputLabel>
<Select
value={String(game)}
label="Game"
onChange={handleChange}
input={<BootstrapInput />}
>
<MenuItem value={1}>Bhop</MenuItem>
<MenuItem value={2}>Surf</MenuItem>
<MenuItem value={3}>Fly Trials</MenuItem>
</Select>
</FormControl>
);
}

@ -0,0 +1,98 @@
"use client"
import { Button, TextField } from "@mui/material"
import GameSelection from "./_game";
import SendIcon from '@mui/icons-material/Send';
import Webpage from "@/app/_components/webpage";
import { useParams } from "next/navigation";
import React, { useState } from "react";
import "./(styles)/page.scss"
interface MapfixPayload {
DisplayName: string;
Creator: string;
GameID: number;
AssetID: number;
AssetVersion: number;
TargetAssetID: number;
}
interface IdResponse {
ID: number;
}
export default function MapfixInfoPage() {
const [game, setGame] = useState(1);
const dynamicId = useParams<{ mapId: string }>();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
const payload: MapfixPayload = {
DisplayName: (formData.get("display-name") as string) ?? "unknown", // TEMPORARY! TODO: Change
Creator: (formData.get("creator") as string) ?? "unknown", // TEMPORARY! TODO: Change
GameID: game,
AssetID: Number((formData.get("asset-id") as string) ?? "0"),
AssetVersion: 0,
TargetAssetID: Number(dynamicId.mapId),
};
console.log(payload)
console.log(JSON.stringify(payload))
try {
// Send the POST request
const response = await fetch("/api/mapfixes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
// Check if the HTTP request was successful
if (!response.ok) {
const errorDetails = await response.text();
// Throw an error with detailed information
throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`);
}
// Allow any HTTP status
const id_response:IdResponse = await response.json();
// navigate to newly created mapfix
window.location.assign(`/mapfixes/${id_response.ID}`)
} catch (error) {
console.error("Error submitting data:", error);
}
};
return (
<Webpage>
<main>
<header>
<h1>Submit Mapfix</h1>
<span className="spacer form-spacer"></span>
</header>
<form onSubmit={handleSubmit}>
{/* TODO: Add form data for mapfixes, such as changes they did, and any times that need to be deleted & what styles */}
<TextField className="form-field" id="display-name" name="display-name" label="Display Name" variant="outlined"/>
<TextField className="form-field" id="creator" name="creator" label="Creator" variant="outlined"/>
<TextField className="form-field" id="asset-id" name="asset-id" label="Asset ID" variant="outlined"/>
{/* I think this is Quat's job to figure this one out (to be set when someone clicks review(?)) */} {/* <TextField className="form-field" id="asset-version" label="Asset Version" variant="outlined"/> */}
<GameSelection game={game} setGame={setGame} />
<span className="spacer form-spacer"></span>
<Button type="submit" variant="contained" startIcon={<SendIcon/>} sx={{
width: "400px",
height: "50px",
marginInline: "auto"
}}>Submit</Button>
</form>
</main>
</Webpage>
)
}

@ -71,7 +71,7 @@ export default function SubmissionInfoPage() {
<Webpage>
<main>
<header>
<h1>Submit Asset</h1>
<h1>Submit New Map</h1>
<span className="spacer form-spacer"></span>
</header>
<form onSubmit={handleSubmit}>

59
web/src/app/ts/Mapfix.ts Normal file

@ -0,0 +1,59 @@
const enum MapfixStatus {
UnderConstruction = 0,
Submitted = 1,
ChangesRequested = 2,
Accepted = 3,
Validating = 4,
Validated = 5,
Uploading = 6,
Uploaded = 7,
Rejected = 8,
}
interface MapfixInfo {
readonly ID: number,
readonly DisplayName: string,
readonly Creator: string,
readonly GameID: number,
readonly Date: number,
readonly Submitter: number,
readonly AssetID: number,
readonly AssetVersion: number,
readonly ValidatedAssetID: number,
readonly ValidatedAssetVersion: number,
readonly Completed: boolean,
readonly TargetAssetID: number,
readonly StatusID: MapfixStatus
readonly StatusMessage: string,
}
function MapfixStatusToString(mapfix_status: MapfixStatus): string {
switch (mapfix_status) {
case MapfixStatus.Rejected:
return "REJECTED"
case MapfixStatus.Uploading:
return "UPLOADING"
case MapfixStatus.Uploaded:
return "UPLOADED"
case MapfixStatus.Validated:
return "VALIDATED"
case MapfixStatus.Validating:
return "VALIDATING"
case MapfixStatus.Accepted:
return "ACCEPTED"
case MapfixStatus.ChangesRequested:
return "CHANGES REQUESTED"
case MapfixStatus.Submitted:
return "SUBMITTED"
case MapfixStatus.UnderConstruction:
return "UNDER CONSTRUCTION"
default:
return "UNKNOWN"
}
}
export {
MapfixStatus,
MapfixStatusToString,
type MapfixInfo
}