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
+}