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}><</button> + <span> + Page {currentPage + 1} of {totalPages} + </span> + <button onClick={nextPage} disabled={currentPage === totalPages - 1}>></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 +}