web: clone submissions page for mapfixes
This commit is contained in:
parent
37560ac5d2
commit
97180ab263
75
web/src/app/mapfixes/(styles)/page.scss
Normal file
75
web/src/app/mapfixes/(styles)/page.scss
Normal file
@ -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;
|
||||||
|
}
|
87
web/src/app/mapfixes/(styles)/page/card.scss
Normal file
87
web/src/app/mapfixes/(styles)/page/card.scss
Normal file
@ -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;
|
||||||
|
}
|
19
web/src/app/mapfixes/[mapfixId]/(styles)/page.scss
Normal file
19
web/src/app/mapfixes/[mapfixId]/(styles)/page.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@forward "./page/commentWindow.scss";
|
||||||
|
@forward "./page/reviewStatus.scss";
|
||||||
|
@forward "./page/ratingWindow.scss";
|
||||||
|
@forward "./page/reviewButtons.scss";
|
||||||
|
@forward "./page/comments.scss";
|
||||||
|
@forward "./page/review.scss";
|
||||||
|
@forward "./page/map.scss";
|
||||||
|
|
||||||
|
@use "../../../globals.scss";
|
||||||
|
|
||||||
|
.map-page-main {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.by-creator {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
@use "../../../../globals.scss";
|
||||||
|
|
||||||
|
#comment-text-field {
|
||||||
|
@include globals.border-with-radius;
|
||||||
|
resize: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
background-color: var(--comment-area)
|
||||||
|
}
|
||||||
|
|
||||||
|
.leave-comment-window {
|
||||||
|
@include globals.border-with-radius;
|
||||||
|
width: 100%;
|
||||||
|
height: 230px;
|
||||||
|
margin-top: 35px;
|
||||||
|
|
||||||
|
.rating-type {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 35%;
|
||||||
|
|
||||||
|
.rating-right {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
margin: 6px 0 6px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 15px 0 15px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--window-header);
|
||||||
|
border-bottom: globals.$review-border;
|
||||||
|
height: 45px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
web/src/app/mapfixes/[mapfixId]/(styles)/page/comments.scss
Normal file
49
web/src/app/mapfixes/[mapfixId]/(styles)/page/comments.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
web/src/app/mapfixes/[mapfixId]/(styles)/page/map.scss
Normal file
15
web/src/app/mapfixes/[mapfixId]/(styles)/page/map.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
@use "../../../../globals.scss";
|
||||||
|
|
||||||
|
.map-image-area {
|
||||||
|
@include globals.border-with-radius;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 350px;
|
||||||
|
height: 350px;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
@use "../../../../globals.scss";
|
||||||
|
|
||||||
|
.rating-window {
|
||||||
|
@include globals.border-with-radius;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.rating-type {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 35%;
|
||||||
|
|
||||||
|
.rating-right {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
margin: 6px 0 6px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 15px 0 15px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--window-header);
|
||||||
|
border-bottom: globals.$review-border;
|
||||||
|
height: 45px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
}
|
47
web/src/app/mapfixes/[mapfixId]/(styles)/page/review.scss
Normal file
47
web/src/app/mapfixes/[mapfixId]/(styles)/page/review.scss
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
@use "../../../../globals.scss";
|
||||||
|
|
||||||
|
.review-info {
|
||||||
|
width: 650px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
p, h1 {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font: {
|
||||||
|
weight: 500;
|
||||||
|
size: 1.8rem
|
||||||
|
};
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--anchor-link-review);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 50px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-area {
|
||||||
|
display: grid;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 25px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 350px;
|
||||||
|
object-fit: contain
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
@use "../../../../globals.scss";
|
||||||
|
|
||||||
|
.review-set {
|
||||||
|
@include globals.border-with-radius;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
$UnderConstruction: "0";
|
||||||
|
$Submitted: "1";
|
||||||
|
$ChangesRequested: "2";
|
||||||
|
$Accepted: "3";
|
||||||
|
$Validating: "4";
|
||||||
|
$Validated: "5";
|
||||||
|
$Uploading: "6";
|
||||||
|
$Uploaded: "7";
|
||||||
|
$Rejected: "8";
|
||||||
|
$Released: "9";
|
||||||
|
|
||||||
|
.review-status {
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 3px 25px 3px 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-review-status="#{$Released}"] {
|
||||||
|
background-color: orange;
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-review-status="#{$Rejected}"] {
|
||||||
|
background-color: orange;
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-review-status="#{$Uploading}"] {
|
||||||
|
background-color: orange;
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-review-status="#{$Uploaded}"] {
|
||||||
|
background-color: orange;
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-review-status="#{$Validated}"] {
|
||||||
|
background-color: orange;
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-review-status="#{$Validating}"] {
|
||||||
|
background-color: orange;
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-review-status="#{$Accepted}"] {
|
||||||
|
background-color: rgb(2, 162, 2);
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-review-status="#{$ChangesRequested}"] {
|
||||||
|
background-color: orange;
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-review-status="#{$Submitted}"] {
|
||||||
|
background-color: orange;
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-review-status="#{$UnderConstruction}"] {
|
||||||
|
background-color: orange;
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
web/src/app/mapfixes/[mapfixId]/_comments.tsx
Normal file
68
web/src/app/mapfixes/[mapfixId]/_comments.tsx
Normal file
@ -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
|
||||||
|
}
|
14
web/src/app/mapfixes/[mapfixId]/_map.tsx
Normal file
14
web/src/app/mapfixes/[mapfixId]/_map.tsx
Normal file
@ -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
|
||||||
|
}
|
74
web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx
Normal file
74
web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx
Normal file
@ -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>
|
||||||
|
)
|
||||||
|
}
|
20
web/src/app/mapfixes/[mapfixId]/_window.tsx
Normal file
20
web/src/app/mapfixes/[mapfixId]/_window.tsx
Normal file
@ -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
|
||||||
|
}
|
105
web/src/app/mapfixes/[mapfixId]/page.tsx
Normal file
105
web/src/app/mapfixes/[mapfixId]/page.tsx
Normal file
@ -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>
|
||||||
|
)
|
||||||
|
}
|
41
web/src/app/mapfixes/_card.tsx
Normal file
41
web/src/app/mapfixes/_card.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
18
web/src/app/mapfixes/_window.tsx
Normal file
18
web/src/app/mapfixes/_window.tsx
Normal file
@ -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
|
||||||
|
}
|
110
web/src/app/mapfixes/page.tsx
Normal file
110
web/src/app/mapfixes/page.tsx
Normal file
@ -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>
|
||||||
|
)
|
||||||
|
}
|
59
web/src/app/ts/Mapfix.ts
Normal file
59
web/src/app/ts/Mapfix.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const enum MapfixStatus {
|
||||||
|
UnderConstruction = 0,
|
||||||
|
Submitted = 1,
|
||||||
|
ChangesRequested = 2,
|
||||||
|
Accepted = 3,
|
||||||
|
Validating = 4,
|
||||||
|
Validated = 5,
|
||||||
|
Uploading = 6,
|
||||||
|
Uploaded = 7,
|
||||||
|
Rejected = 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MapfixInfo {
|
||||||
|
readonly ID: number,
|
||||||
|
readonly DisplayName: string,
|
||||||
|
readonly Creator: string,
|
||||||
|
readonly GameID: number,
|
||||||
|
readonly Date: number,
|
||||||
|
readonly Submitter: number,
|
||||||
|
readonly AssetID: number,
|
||||||
|
readonly AssetVersion: number,
|
||||||
|
readonly ValidatedAssetID: number,
|
||||||
|
readonly ValidatedAssetVersion: number,
|
||||||
|
readonly Completed: boolean,
|
||||||
|
readonly TargetAssetID: number,
|
||||||
|
readonly StatusID: MapfixStatus
|
||||||
|
readonly StatusMessage: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
function MapfixStatusToString(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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user