From 97180ab2637cee07c46278f1ba838df64f7c69d3 Mon Sep 17 00:00:00 2001
From: Quaternions <krakow20@gmail.com>
Date: Tue, 1 Apr 2025 14:15:03 -0700
Subject: [PATCH 1/4] web: clone submissions page for mapfixes

---
 web/src/app/mapfixes/(styles)/page.scss       |  75 ++++++++++++
 web/src/app/mapfixes/(styles)/page/card.scss  |  87 ++++++++++++++
 .../mapfixes/[mapfixId]/(styles)/page.scss    |  19 +++
 .../(styles)/page/commentWindow.scss          |  56 +++++++++
 .../[mapfixId]/(styles)/page/comments.scss    |  49 ++++++++
 .../[mapfixId]/(styles)/page/map.scss         |  15 +++
 .../(styles)/page/ratingWindow.scss           |  43 +++++++
 .../[mapfixId]/(styles)/page/review.scss      |  47 ++++++++
 .../(styles)/page/reviewButtons.scss          |  13 +++
 .../(styles)/page/reviewStatus.scss           |  80 +++++++++++++
 web/src/app/mapfixes/[mapfixId]/_comments.tsx |  68 +++++++++++
 web/src/app/mapfixes/[mapfixId]/_map.tsx      |  14 +++
 .../mapfixes/[mapfixId]/_reviewButtons.tsx    |  74 ++++++++++++
 web/src/app/mapfixes/[mapfixId]/_window.tsx   |  20 ++++
 web/src/app/mapfixes/[mapfixId]/page.tsx      | 105 +++++++++++++++++
 web/src/app/mapfixes/_card.tsx                |  41 +++++++
 web/src/app/mapfixes/_window.tsx              |  18 +++
 web/src/app/mapfixes/page.tsx                 | 110 ++++++++++++++++++
 web/src/app/ts/Mapfix.ts                      |  59 ++++++++++
 19 files changed, 993 insertions(+)
 create mode 100644 web/src/app/mapfixes/(styles)/page.scss
 create mode 100644 web/src/app/mapfixes/(styles)/page/card.scss
 create mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page.scss
 create mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/commentWindow.scss
 create mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/comments.scss
 create mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/map.scss
 create mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/ratingWindow.scss
 create mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/review.scss
 create mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewButtons.scss
 create mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewStatus.scss
 create mode 100644 web/src/app/mapfixes/[mapfixId]/_comments.tsx
 create mode 100644 web/src/app/mapfixes/[mapfixId]/_map.tsx
 create mode 100644 web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx
 create mode 100644 web/src/app/mapfixes/[mapfixId]/_window.tsx
 create mode 100644 web/src/app/mapfixes/[mapfixId]/page.tsx
 create mode 100644 web/src/app/mapfixes/_card.tsx
 create mode 100644 web/src/app/mapfixes/_window.tsx
 create mode 100644 web/src/app/mapfixes/page.tsx
 create mode 100644 web/src/app/ts/Mapfix.ts

diff --git a/web/src/app/mapfixes/(styles)/page.scss b/web/src/app/mapfixes/(styles)/page.scss
new file mode 100644
index 0000000..bf98951
--- /dev/null
+++ b/web/src/app/mapfixes/(styles)/page.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/(styles)/page/card.scss b/web/src/app/mapfixes/(styles)/page/card.scss
new file mode 100644
index 0000000..dce43eb
--- /dev/null
+++ b/web/src/app/mapfixes/(styles)/page/card.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page.scss
new file mode 100644
index 0000000..4015f24
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/(styles)/page.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/commentWindow.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/commentWindow.scss
new file mode 100644
index 0000000..d04b140
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/(styles)/page/commentWindow.scss
@@ -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;
+        }
+    }
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/comments.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/comments.scss
new file mode 100644
index 0000000..d74b8a6
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/(styles)/page/comments.scss
@@ -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;
+            }
+        }
+    }
+}
diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/map.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/map.scss
new file mode 100644
index 0000000..ede388e
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/(styles)/page/map.scss
@@ -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;
+	}
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/ratingWindow.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/ratingWindow.scss
new file mode 100644
index 0000000..770fe16
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/(styles)/page/ratingWindow.scss
@@ -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;
+    }
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/review.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/review.scss
new file mode 100644
index 0000000..08fc5c0
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/(styles)/page/review.scss
@@ -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
+    }
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewButtons.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewButtons.scss
new file mode 100644
index 0000000..ccc6ecd
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewButtons.scss
@@ -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%;
+	}
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewStatus.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewStatus.scss
new file mode 100644
index 0000000..e64bd43
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewStatus.scss
@@ -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;
+        }
+    }
+}
diff --git a/web/src/app/mapfixes/[mapfixId]/_comments.tsx b/web/src/app/mapfixes/[mapfixId]/_comments.tsx
new file mode 100644
index 0000000..8e5c84a
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/_comments.tsx
@@ -0,0 +1,68 @@
+import type { SubmissionInfo } from "@/app/ts/Submission";
+import { Button } from "@mui/material"
+import Window from "./_window";
+import SendIcon from '@mui/icons-material/Send';
+import Image from "next/image";
+
+interface CommentersProps {
+    comments_data: CreatorAndReviewStatus
+}
+
+interface CreatorAndReviewStatus {
+    asset_id: SubmissionInfo["AssetID"],
+    creator: SubmissionInfo["DisplayName"],
+    review: SubmissionInfo["StatusID"],
+    status_message: SubmissionInfo["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
+}
diff --git a/web/src/app/mapfixes/[mapfixId]/_map.tsx b/web/src/app/mapfixes/[mapfixId]/_map.tsx
new file mode 100644
index 0000000..e364f70
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/_map.tsx
@@ -0,0 +1,14 @@
+import { SubmissionInfo } from "@/app/ts/Submission"
+
+interface AssetID {
+    id: SubmissionInfo["AssetID"]
+}
+
+function MapImage() {
+    return <p>Fetching map image...</p>
+}
+
+export {
+	type AssetID,
+	MapImage
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx b/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx
new file mode 100644
index 0000000..822327c
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx
@@ -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,
+	submissionId: string,
+	color: ButtonOwnProps["color"]
+}
+
+interface ReviewId {
+	submissionId: string
+}
+
+async function ReviewButtonClicked(action: ApiActions, submissionId: string) {
+	try {
+		const response = await fetch(`/api/submissions/${submissionId}/status/${action}`, {
+			method: "POST",
+			headers: {
+				"Content-type": "application/json",
+			}
+		});
+		// Check if the HTTP request was successful
+		if (!response.ok) {
+			const errorDetails = await response.text();
+
+			// Throw an error with detailed information
+			throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`);
+		}
+
+		window.location.reload();
+	} catch (error) {
+		console.error("Error updating submission status:", error);
+	}
+}
+
+function ReviewButton(props: ReviewButton) {
+	return <Button
+		color={props.color}
+		variant="contained"
+		onClick={() => { ReviewButtonClicked(props.action, props.submissionId) }}>{props.name}</Button>
+}
+
+export default function ReviewButtons(props: ReviewId) {
+	const submissionId = props.submissionId
+	// 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"           submissionId={submissionId}/>
+			<ReviewButton color="info"  name="Revoke"    action="revoke"           submissionId={submissionId}/>
+			<ReviewButton color="info"  name="Accept"    action="trigger-validate" submissionId={submissionId}/>
+			<ReviewButton color="info"  name="Validate"  action="retry-validate"   submissionId={submissionId}/>
+			<ReviewButton color="error" name="Reject"    action="reject"           submissionId={submissionId}/>
+			<ReviewButton color="info"  name="Upload"    action="trigger-upload"   submissionId={submissionId}/>
+			<ReviewButton color="error" name="Reset Uploading (fix softlocked status)"    action="reset-uploading"   submissionId={submissionId}/>
+			<ReviewButton color="error" name="Reset Validating (fix softlocked status)"   action="reset-validating"  submissionId={submissionId}/>
+		</section>
+	)
+}
diff --git a/web/src/app/mapfixes/[mapfixId]/_window.tsx b/web/src/app/mapfixes/[mapfixId]/_window.tsx
new file mode 100644
index 0000000..866b5a4
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/_window.tsx
@@ -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
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/[mapfixId]/page.tsx b/web/src/app/mapfixes/[mapfixId]/page.tsx
new file mode 100644
index 0000000..9dae81f
--- /dev/null
+++ b/web/src/app/mapfixes/[mapfixId]/page.tsx
@@ -0,0 +1,105 @@
+"use client"
+
+import { SubmissionInfo, SubmissionStatusToString } from "@/app/ts/Submission";
+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 {
+	submissionId: 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(submission: ReviewId) {
+    return (
+        <aside className="review-area">
+        	<section className="map-image-area">
+         		<MapImage/>
+         	</section>
+            <Ratings/>
+			<ReviewButtons submissionId={submission.submissionId}/>
+        </aside>
+    )
+}
+
+function TitleAndComments(stats: CreatorAndReviewStatus) {
+    const Review = SubmissionStatusToString(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 SubmissionInfoPage() {
+    const dynamicId = useParams<{submissionId: string}>()
+
+    const [submission, setSubmission] = useState<SubmissionInfo | 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 getSubmission() {
+			const res = await fetch(`/api/submissions/${dynamicId.submissionId}`)
+			if (res.ok) {
+				setSubmission(await res.json())
+			}
+		}
+        getSubmission()
+	}, [dynamicId.submissionId])
+
+	if (!submission) {
+		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 submissionId={dynamicId.submissionId}/>
+                    <TitleAndComments name={submission.DisplayName} creator={submission.Creator} review={submission.StatusID} status_message={submission.StatusMessage} asset_id={submission.AssetID} comments={[]}/>
+                </section>
+            </main>
+        </Webpage>
+	)
+}
diff --git a/web/src/app/mapfixes/_card.tsx b/web/src/app/mapfixes/_card.tsx
new file mode 100644
index 0000000..29a6793
--- /dev/null
+++ b/web/src/app/mapfixes/_card.tsx
@@ -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>
+	);
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/_window.tsx b/web/src/app/mapfixes/_window.tsx
new file mode 100644
index 0000000..b71e7e2
--- /dev/null
+++ b/web/src/app/mapfixes/_window.tsx
@@ -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
+}
\ No newline at end of file
diff --git a/web/src/app/mapfixes/page.tsx b/web/src/app/mapfixes/page.tsx
new file mode 100644
index 0000000..2b803a7
--- /dev/null
+++ b/web/src/app/mapfixes/page.tsx
@@ -0,0 +1,110 @@
+'use client'
+
+import React, { useState, useEffect } from "react";
+import { SubmissionInfo } from "../ts/Submission";
+import SubmissionCard from "./_card";
+import Webpage from "@/app/_components/webpage";
+
+import "./(styles)/page.scss";
+
+export default function SubmissionInfoPage() {
+	const [submissions, setSubmissions] = useState<SubmissionInfo[]>([])
+	const [currentPage, setCurrentPage] = useState(0);
+	const cardsPerPage = 24; // built to fit on a 1920x1080 monitor
+
+	const totalPages = Math.ceil(submissions.length / cardsPerPage);
+
+	const currentCards = submissions.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 fetchSubmissions() {
+			const res = await fetch('/api/submissions?Page=1&Limit=100')
+			if (res.ok) {
+				setSubmissions(await res.json())
+			}
+		}
+
+		setTimeout(() => {
+			fetchSubmissions()
+		}, 50);
+	}, [])
+
+	if (!submissions) {
+		return <Webpage>
+			<main>
+				Loading...
+			</main>
+		</Webpage>
+	}
+
+	if (submissions && submissions.length == 0) {
+		return <Webpage>
+			<main>
+				Submissions 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((submission) => (
+						<SubmissionCard
+							key={submission.ID}
+							id={submission.ID}
+							assetId={submission.AssetID}
+							displayName={submission.DisplayName}
+							author={submission.Creator}
+							rating={submission.StatusID}
+						/>
+					))}
+				</div>
+			</main>
+		</Webpage>
+	)
+}
diff --git a/web/src/app/ts/Mapfix.ts b/web/src/app/ts/Mapfix.ts
new file mode 100644
index 0000000..7888436
--- /dev/null
+++ b/web/src/app/ts/Mapfix.ts
@@ -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(submission_status: MapfixStatus): string {
+	switch (submission_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
+}
-- 
2.47.1


From 146d627534e766c53e4f81237d49561c208addeb Mon Sep 17 00:00:00 2001
From: Quaternions <krakow20@gmail.com>
Date: Tue, 1 Apr 2025 14:24:36 -0700
Subject: [PATCH 2/4] web: mapfixes: rename all occurrences of submission with
 mapfix

---
 web/src/app/mapfixes/[mapfixId]/_comments.tsx | 10 ++---
 web/src/app/mapfixes/[mapfixId]/_map.tsx      |  6 +--
 .../mapfixes/[mapfixId]/_reviewButtons.tsx    | 30 ++++++-------
 web/src/app/mapfixes/[mapfixId]/page.tsx      | 32 +++++++-------
 web/src/app/mapfixes/_card.tsx                |  2 +-
 web/src/app/mapfixes/page.tsx                 | 42 +++++++++----------
 web/src/app/ts/Mapfix.ts                      |  4 +-
 7 files changed, 63 insertions(+), 63 deletions(-)

diff --git a/web/src/app/mapfixes/[mapfixId]/_comments.tsx b/web/src/app/mapfixes/[mapfixId]/_comments.tsx
index 8e5c84a..7c5286a 100644
--- a/web/src/app/mapfixes/[mapfixId]/_comments.tsx
+++ b/web/src/app/mapfixes/[mapfixId]/_comments.tsx
@@ -1,4 +1,4 @@
-import type { SubmissionInfo } from "@/app/ts/Submission";
+import type { MapfixInfo } from "@/app/ts/Mapfix";
 import { Button } from "@mui/material"
 import Window from "./_window";
 import SendIcon from '@mui/icons-material/Send';
@@ -9,10 +9,10 @@ interface CommentersProps {
 }
 
 interface CreatorAndReviewStatus {
-    asset_id: SubmissionInfo["AssetID"],
-    creator: SubmissionInfo["DisplayName"],
-    review: SubmissionInfo["StatusID"],
-    status_message: SubmissionInfo["StatusMessage"],
+    asset_id: MapfixInfo["AssetID"],
+    creator: MapfixInfo["DisplayName"],
+    review: MapfixInfo["StatusID"],
+    status_message: MapfixInfo["StatusMessage"],
     comments: Comment[],
     name: string
 }
diff --git a/web/src/app/mapfixes/[mapfixId]/_map.tsx b/web/src/app/mapfixes/[mapfixId]/_map.tsx
index e364f70..f515db2 100644
--- a/web/src/app/mapfixes/[mapfixId]/_map.tsx
+++ b/web/src/app/mapfixes/[mapfixId]/_map.tsx
@@ -1,7 +1,7 @@
-import { SubmissionInfo } from "@/app/ts/Submission"
+import { MapfixInfo } from "@/app/ts/Mapfix"
 
 interface AssetID {
-    id: SubmissionInfo["AssetID"]
+    id: MapfixInfo["AssetID"]
 }
 
 function MapImage() {
@@ -11,4 +11,4 @@ function MapImage() {
 export {
 	type AssetID,
 	MapImage
-}
\ No newline at end of file
+}
diff --git a/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx b/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx
index 822327c..24d75c4 100644
--- a/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx
+++ b/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx
@@ -7,17 +7,17 @@ type Review     = Actions | "Accept" | "Validate" | "Upload" | "Reset Uploading
 interface ReviewButton {
 	name: Review,
 	action: ApiActions,
-	submissionId: string,
+	mapfixId: string,
 	color: ButtonOwnProps["color"]
 }
 
 interface ReviewId {
-	submissionId: string
+	mapfixId: string
 }
 
-async function ReviewButtonClicked(action: ApiActions, submissionId: string) {
+async function ReviewButtonClicked(action: ApiActions, mapfixId: string) {
 	try {
-		const response = await fetch(`/api/submissions/${submissionId}/status/${action}`, {
+		const response = await fetch(`/api/mapfixes/${mapfixId}/status/${action}`, {
 			method: "POST",
 			headers: {
 				"Content-type": "application/json",
@@ -33,7 +33,7 @@ async function ReviewButtonClicked(action: ApiActions, submissionId: string) {
 
 		window.location.reload();
 	} catch (error) {
-		console.error("Error updating submission status:", error);
+		console.error("Error updating mapfix status:", error);
 	}
 }
 
@@ -41,11 +41,11 @@ function ReviewButton(props: ReviewButton) {
 	return <Button
 		color={props.color}
 		variant="contained"
-		onClick={() => { ReviewButtonClicked(props.action, props.submissionId) }}>{props.name}</Button>
+		onClick={() => { ReviewButtonClicked(props.action, props.mapfixId) }}>{props.name}</Button>
 }
 
 export default function ReviewButtons(props: ReviewId) {
-	const submissionId = props.submissionId
+	const mapfixId = props.mapfixId
 	// When is each button visible?
 	// Multiple buttons can be visible at once.
 	// Action         | Role      | When Current Status is One of:
@@ -61,14 +61,14 @@ export default function ReviewButtons(props: ReviewId) {
 	// ResetUploading | MapAdmin  | Uploading
 	return (
 		<section className="review-set">
-			<ReviewButton color="info"  name="Submit"    action="submit"           submissionId={submissionId}/>
-			<ReviewButton color="info"  name="Revoke"    action="revoke"           submissionId={submissionId}/>
-			<ReviewButton color="info"  name="Accept"    action="trigger-validate" submissionId={submissionId}/>
-			<ReviewButton color="info"  name="Validate"  action="retry-validate"   submissionId={submissionId}/>
-			<ReviewButton color="error" name="Reject"    action="reject"           submissionId={submissionId}/>
-			<ReviewButton color="info"  name="Upload"    action="trigger-upload"   submissionId={submissionId}/>
-			<ReviewButton color="error" name="Reset Uploading (fix softlocked status)"    action="reset-uploading"   submissionId={submissionId}/>
-			<ReviewButton color="error" name="Reset Validating (fix softlocked status)"   action="reset-validating"  submissionId={submissionId}/>
+			<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>
 	)
 }
diff --git a/web/src/app/mapfixes/[mapfixId]/page.tsx b/web/src/app/mapfixes/[mapfixId]/page.tsx
index 9dae81f..cc38cb3 100644
--- a/web/src/app/mapfixes/[mapfixId]/page.tsx
+++ b/web/src/app/mapfixes/[mapfixId]/page.tsx
@@ -1,6 +1,6 @@
 "use client"
 
-import { SubmissionInfo, SubmissionStatusToString } from "@/app/ts/Submission";
+import { MapfixInfo, MapfixStatusToString } from "@/app/ts/Mapfix";
 import type { CreatorAndReviewStatus } from "./_comments";
 import { MapImage } from "./_map";
 import { useParams } from "next/navigation";
@@ -15,7 +15,7 @@ import { useState, useEffect } from "react";
 import "./(styles)/page.scss";
 
 interface ReviewId {
-	submissionId: string
+	mapfixId: string
 }
 
 function Ratings() {
@@ -39,20 +39,20 @@ function Ratings() {
 	)
 }
 
-function RatingArea(submission: ReviewId) {
+function RatingArea(mapfix: ReviewId) {
     return (
         <aside className="review-area">
         	<section className="map-image-area">
          		<MapImage/>
          	</section>
             <Ratings/>
-			<ReviewButtons submissionId={submission.submissionId}/>
+			<ReviewButtons mapfixId={mapfix.mapfixId}/>
         </aside>
     )
 }
 
 function TitleAndComments(stats: CreatorAndReviewStatus) {
-    const Review = SubmissionStatusToString(stats.review)
+    const Review = MapfixStatusToString(stats.review)
 
     // TODO: hide status message when status is not "Accepted"
 	return (
@@ -72,22 +72,22 @@ function TitleAndComments(stats: CreatorAndReviewStatus) {
     )
 }
 
-export default function SubmissionInfoPage() {
-    const dynamicId = useParams<{submissionId: string}>()
+export default function MapfixInfoPage() {
+    const dynamicId = useParams<{mapfixId: string}>()
 
-    const [submission, setSubmission] = useState<SubmissionInfo | null>(null)
+    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 getSubmission() {
-			const res = await fetch(`/api/submissions/${dynamicId.submissionId}`)
+		async function getMapfix() {
+			const res = await fetch(`/api/mapfixes/${dynamicId.mapfixId}`)
 			if (res.ok) {
-				setSubmission(await res.json())
+				setMapfix(await res.json())
 			}
 		}
-        getSubmission()
-	}, [dynamicId.submissionId])
+        getMapfix()
+	}, [dynamicId.mapfixId])
 
-	if (!submission) {
+	if (!mapfix) {
 		return <Webpage>
 		        {/* TODO: Add skeleton loading thingy ? Maybe ? (https://mui.com/material-ui/react-skeleton/) */}
 			</Webpage>
@@ -96,8 +96,8 @@ export default function SubmissionInfoPage() {
         <Webpage>
             <main className="map-page-main">
                 <section className="review-section">
-					<RatingArea submissionId={dynamicId.submissionId}/>
-                    <TitleAndComments name={submission.DisplayName} creator={submission.Creator} review={submission.StatusID} status_message={submission.StatusMessage} asset_id={submission.AssetID} comments={[]}/>
+					<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>
diff --git a/web/src/app/mapfixes/_card.tsx b/web/src/app/mapfixes/_card.tsx
index 29a6793..a1c594a 100644
--- a/web/src/app/mapfixes/_card.tsx
+++ b/web/src/app/mapfixes/_card.tsx
@@ -38,4 +38,4 @@ export default function SubmissionCard(props: SubmissionCardProps) {
 			</div>
 		</Link>
 	);
-}
\ No newline at end of file
+}
diff --git a/web/src/app/mapfixes/page.tsx b/web/src/app/mapfixes/page.tsx
index 2b803a7..cf6eccb 100644
--- a/web/src/app/mapfixes/page.tsx
+++ b/web/src/app/mapfixes/page.tsx
@@ -1,20 +1,20 @@
 'use client'
 
 import React, { useState, useEffect } from "react";
-import { SubmissionInfo } from "../ts/Submission";
-import SubmissionCard from "./_card";
+import { MapfixInfo } from "../ts/Mapfix";
+import MapfixCard from "./_card";
 import Webpage from "@/app/_components/webpage";
 
 import "./(styles)/page.scss";
 
-export default function SubmissionInfoPage() {
-	const [submissions, setSubmissions] = useState<SubmissionInfo[]>([])
+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(submissions.length / cardsPerPage);
+	const totalPages = Math.ceil(mapfixes.length / cardsPerPage);
 
-	const currentCards = submissions.slice(
+	const currentCards = mapfixes.slice(
 		currentPage * cardsPerPage,
 		(currentPage + 1) * cardsPerPage
 	);
@@ -32,19 +32,19 @@ export default function SubmissionInfoPage() {
 	};
 
 	useEffect(() => {
-		async function fetchSubmissions() {
-			const res = await fetch('/api/submissions?Page=1&Limit=100')
+		async function fetchMapfixes() {
+			const res = await fetch('/api/mapfixes?Page=1&Limit=100')
 			if (res.ok) {
-				setSubmissions(await res.json())
+				setMapfixes(await res.json())
 			}
 		}
 
 		setTimeout(() => {
-			fetchSubmissions()
+			fetchMapfixes()
 		}, 50);
 	}, [])
 
-	if (!submissions) {
+	if (!mapfixes) {
 		return <Webpage>
 			<main>
 				Loading...
@@ -52,10 +52,10 @@ export default function SubmissionInfoPage() {
 		</Webpage>
 	}
 
-	if (submissions && submissions.length == 0) {
+	if (mapfixes && mapfixes.length == 0) {
 		return <Webpage>
 			<main>
-				Submissions list is empty.
+				Mapfixes list is empty.
 			</main>
 		</Webpage>
 	}
@@ -93,14 +93,14 @@ export default function SubmissionInfoPage() {
 					<button onClick={nextPage} disabled={currentPage === totalPages - 1}>&gt;</button>
 				</div>
 				<div className="grid">
-					{currentCards.map((submission) => (
-						<SubmissionCard
-							key={submission.ID}
-							id={submission.ID}
-							assetId={submission.AssetID}
-							displayName={submission.DisplayName}
-							author={submission.Creator}
-							rating={submission.StatusID}
+					{currentCards.map((mapfix) => (
+						<MapfixCard
+							key={mapfix.ID}
+							id={mapfix.ID}
+							assetId={mapfix.AssetID}
+							displayName={mapfix.DisplayName}
+							author={mapfix.Creator}
+							rating={mapfix.StatusID}
 						/>
 					))}
 				</div>
diff --git a/web/src/app/ts/Mapfix.ts b/web/src/app/ts/Mapfix.ts
index 7888436..0a5bd25 100644
--- a/web/src/app/ts/Mapfix.ts
+++ b/web/src/app/ts/Mapfix.ts
@@ -27,8 +27,8 @@ interface MapfixInfo {
     readonly StatusMessage: string,
 }
 
-function MapfixStatusToString(submission_status: MapfixStatus): string {
-	switch (submission_status) {
+function MapfixStatusToString(mapfix_status: MapfixStatus): string {
+	switch (mapfix_status) {
    	case MapfixStatus.Rejected:
     		return "REJECTED"
 		case MapfixStatus.Uploading:
-- 
2.47.1


From 4cf7889db937a38209143dcd243126a4db04fea6 Mon Sep 17 00:00:00 2001
From: Quaternions <krakow20@gmail.com>
Date: Tue, 1 Apr 2025 14:58:58 -0700
Subject: [PATCH 3/4] web: add submit page at /maps/[mapId]/fix

---
 .../app/maps/[mapId]/fix/(styles)/page.scss   | 54 ++++++++++
 web/src/app/maps/[mapId]/fix/_game.tsx        | 65 ++++++++++++
 web/src/app/maps/[mapId]/fix/page.tsx         | 98 +++++++++++++++++++
 3 files changed, 217 insertions(+)
 create mode 100644 web/src/app/maps/[mapId]/fix/(styles)/page.scss
 create mode 100644 web/src/app/maps/[mapId]/fix/_game.tsx
 create mode 100644 web/src/app/maps/[mapId]/fix/page.tsx

diff --git a/web/src/app/maps/[mapId]/fix/(styles)/page.scss b/web/src/app/maps/[mapId]/fix/(styles)/page.scss
new file mode 100644
index 0000000..8c7dcf8
--- /dev/null
+++ b/web/src/app/maps/[mapId]/fix/(styles)/page.scss
@@ -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
+	}
+}
diff --git a/web/src/app/maps/[mapId]/fix/_game.tsx b/web/src/app/maps/[mapId]/fix/_game.tsx
new file mode 100644
index 0000000..e754601
--- /dev/null
+++ b/web/src/app/maps/[mapId]/fix/_game.tsx
@@ -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>
+	);
+}
\ No newline at end of file
diff --git a/web/src/app/maps/[mapId]/fix/page.tsx b/web/src/app/maps/[mapId]/fix/page.tsx
new file mode 100644
index 0000000..5e68374
--- /dev/null
+++ b/web/src/app/maps/[mapId]/fix/page.tsx
@@ -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>
+	)
+}
-- 
2.47.1


From a119c4292ead3aee8c65d1bd91afa13b4be19bfb Mon Sep 17 00:00:00 2001
From: Quaternions <krakow20@gmail.com>
Date: Tue, 1 Apr 2025 15:01:28 -0700
Subject: [PATCH 4/4] web: change submit text to match mapfix submit page

---
 web/src/app/submit/page.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web/src/app/submit/page.tsx b/web/src/app/submit/page.tsx
index 38cc9e3..b7108e9 100644
--- a/web/src/app/submit/page.tsx
+++ b/web/src/app/submit/page.tsx
@@ -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}>
-- 
2.47.1