map page now builds and heavily refactored
+ Handle no commenters by displaying the message "There are no commenters." + Text fallback: "Fetching map image..." + Handle the validation status message (still has no css coloring for 80% of the statuses) - Disable the roblox api for fetching the asset image and avatar headshot images, but keep the module
This commit is contained in:
parent
37e4e29f04
commit
1a5f58a3b1
@ -1,5 +1,8 @@
|
|||||||
import "./globals.scss";
|
import "./globals.scss";
|
||||||
|
|
||||||
|
interface Component {
|
||||||
|
|
||||||
|
}
|
||||||
export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) {
|
export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
@ -5,29 +5,29 @@ import Header from "@/app/_components/header";
|
|||||||
import "./styles/layout.scss";
|
import "./styles/layout.scss";
|
||||||
|
|
||||||
export default function MapPage() {
|
export default function MapPage() {
|
||||||
const placeholder_Comment = [
|
// const placeholder_Comments = [
|
||||||
{
|
// {
|
||||||
comment: "This map has been accepted and is in the game.",
|
// comment: "This map has been accepted and is in the game.",
|
||||||
date: "on Dec 8 '24 at 18:46",
|
// date: "on Dec 8 '24 at 18:46",
|
||||||
name: "BhopMaptest"
|
// name: "BhopMaptest"
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
comment: "This map is so mid...",
|
// comment: "This map is so mid...",
|
||||||
date: "on Dec 8 '24 at 18:46",
|
// date: "on Dec 8 '24 at 18:46",
|
||||||
name: "vmsize"
|
// name: "vmsize"
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
comment: "I prefer strafe client",
|
// comment: "I prefer strafe client",
|
||||||
date: "on Dec 8 '24 at 18:46",
|
// date: "on Dec 8 '24 at 18:46",
|
||||||
name: "Quaternions"
|
// name: "Quaternions"
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header/>
|
<Header/>
|
||||||
<main className="map-page-main">
|
<main className="map-page-main">
|
||||||
<MapInfoPage assetid={14783300138} status={SubmissionStatus.Accepted} comments={placeholder_Comment}/>
|
<MapInfoPage assetid={14783300138} status={SubmissionStatus.Accepted} comments={[]}/>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
26
web/src/app/map/[map_name]/map.tsx
Normal file
26
web/src/app/map/[map_name]/map.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { SubmissionInfo } from "@/app/ts/Submission"
|
||||||
|
import { AssetImage } from "@/app/ts/Roblox"
|
||||||
|
import Image from "next/image"
|
||||||
|
|
||||||
|
interface AssetID {
|
||||||
|
id: SubmissionInfo["AssetID"]
|
||||||
|
}
|
||||||
|
function MapImage(asset: AssetID) {
|
||||||
|
const [assetImage, setAssetImage] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
AssetImage(asset.id, "420x420").then(image => setAssetImage(image))
|
||||||
|
}, []);
|
||||||
|
if (!assetImage) {
|
||||||
|
return <p>Fetching map image...</p>;
|
||||||
|
}
|
||||||
|
return <Image src={assetImage} alt="Map Image"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
type AssetID,
|
||||||
|
MapImage
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
import { SubmissionStatus, type SubmissionInfo } from "@/app/ts/Submission";
|
"use client"
|
||||||
import { useState, type ReactNode } from "react";
|
|
||||||
|
import { SubmissionStatus, SubmissionStatusToString, type SubmissionInfo } from "@/app/ts/Submission";
|
||||||
|
import { MapImage, type AssetID } from "./map";
|
||||||
import { Rating, Button } from "@mui/material";
|
import { Rating, Button } from "@mui/material";
|
||||||
import { AssetImage } from "@/app/ts/Roblox";
|
import type {ReactNode} from "react";
|
||||||
import SendIcon from '@mui/icons-material/Send';
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import "./styles/page.scss";
|
import "./styles/page.scss";
|
||||||
|
|
||||||
@ -11,6 +14,23 @@ interface Window {
|
|||||||
title: string,
|
title: string,
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
interface Comment {
|
||||||
|
picture?: string, //TEMP
|
||||||
|
comment: string,
|
||||||
|
date: string,
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
interface CreatorAndReviewStatus {
|
||||||
|
creator: SubmissionInfo["DisplayName"],
|
||||||
|
review: SubmissionInfo["StatusID"],
|
||||||
|
comments: Comment[]
|
||||||
|
}
|
||||||
|
interface MapInfo {
|
||||||
|
assetid: SubmissionInfo["AssetID"],
|
||||||
|
status: SubmissionStatus,
|
||||||
|
comments: Comment[]
|
||||||
|
}
|
||||||
|
|
||||||
function Window(window: Window) {
|
function Window(window: Window) {
|
||||||
return (
|
return (
|
||||||
<section className={window.className}>
|
<section className={window.className}>
|
||||||
@ -22,17 +42,11 @@ function Window(window: Window) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AssetID {
|
function ImageAndRatings(asset: AssetID) {
|
||||||
id: SubmissionInfo["AssetID"]
|
|
||||||
}
|
|
||||||
async function ImageAndRatings(asset: AssetID) {
|
|
||||||
const [assetImage, setAssetImage] = useState(null);
|
|
||||||
const asset_image = await AssetImage(asset.id, "420x420")
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="review-area">
|
<aside className="review-area">
|
||||||
<section className="rating">
|
<section className="map-image-area">
|
||||||
<img src={asset_image} alt="Map Image"/>
|
<MapImage id={asset.id}/>
|
||||||
</section>
|
</section>
|
||||||
<Window className="rating-window" title="Rating">
|
<Window className="rating-window" title="Rating">
|
||||||
<section className="rating-type">
|
<section className="rating-type">
|
||||||
@ -54,28 +68,12 @@ async function ImageAndRatings(asset: AssetID) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Comment {
|
|
||||||
picture?: string, //TEMP
|
|
||||||
comment: string,
|
|
||||||
date: string,
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
function Comment(comment: Comment) {
|
function Comment(comment: Comment) {
|
||||||
let placeHolder;
|
const IsBhopMaptest = comment.name == "BhopMaptest" //Highlighted commenter
|
||||||
if (comment.name === "BhopMaptest") {
|
|
||||||
placeHolder = "https://tr.rbxcdn.com/30DAY-AvatarHeadshot-FB29ADF0A483B2745DB2571DC4785202-Png/150/150/AvatarHeadshot/Webp/noFilter"
|
|
||||||
} else if (comment.name === "vmsize") {
|
|
||||||
placeHolder = "https://tr.rbxcdn.com/30DAY-AvatarHeadshot-ACEB71FADC70B458ECB9D6AA9AAE5913-Png/150/150/AvatarHeadshot/Webp/noFilter"
|
|
||||||
} else {
|
|
||||||
placeHolder = "https://tr.rbxcdn.com/30DAY-AvatarHeadshot-1ED6D3ED61793733397BB596F0ADD369-Png/150/150/AvatarHeadshot/Webp/noFilter"
|
|
||||||
}
|
|
||||||
|
|
||||||
//Highlighted comment
|
|
||||||
const IsBhopMaptest = comment.name == "BhopMaptest"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="commenter" data-highlighted={IsBhopMaptest}>
|
<div className="commenter" data-highlighted={IsBhopMaptest}>
|
||||||
<img src={placeHolder} alt={`${comment.name}'s comment`}/>
|
<img src={comment.picture} alt={`${comment.name}'s comment`}/>
|
||||||
<div className="details">
|
<div className="details">
|
||||||
<header>
|
<header>
|
||||||
<p className="name">{comment.name}</p>
|
<p className="name">{comment.name}</p>
|
||||||
@ -87,28 +85,24 @@ function Comment(comment: Comment) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreatorAndReviewStatus {
|
|
||||||
creator: SubmissionInfo["DisplayName"],
|
|
||||||
review: SubmissionInfo["StatusID"],
|
|
||||||
comments: Comment[]
|
|
||||||
}
|
|
||||||
function TitleAndComments(stats: CreatorAndReviewStatus) {
|
function TitleAndComments(stats: CreatorAndReviewStatus) {
|
||||||
//TODO: switch case this for matching the enums
|
const Review = SubmissionStatusToString(stats.review)
|
||||||
return (
|
|
||||||
|
return (
|
||||||
<main className="review-info">
|
<main className="review-info">
|
||||||
<div>
|
<div>
|
||||||
<h1>bhop_quaternions</h1>
|
<h1>bhop_quaternions</h1>
|
||||||
<aside data-review-status={stats.review} className="review-status">
|
<aside data-review-status={stats.review} className="review-status">
|
||||||
<p>ACCEPTED</p>
|
<p>{Review}</p>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
<p className="by-creator">by <a href="" target="_blank">{stats.creator}</a></p>
|
<p className="by-creator">by <Link href="" target="_blank">{stats.creator}</Link></p>
|
||||||
<span className="spacer"></span>
|
<span className="spacer"></span>
|
||||||
<main className="comments">
|
<section className="comments">
|
||||||
{stats.comments.map(comment => (
|
{stats.comments.length===0 && <p className="no-comments">There are no comments.</p> || stats.comments.map(comment => (
|
||||||
<Comment name={comment.name} date={comment.date} comment={comment.comment}/>
|
<Comment key={comment.name} name={comment.name} date={comment.date} comment={comment.comment}/>
|
||||||
))}
|
))}
|
||||||
</main>
|
</section>
|
||||||
<Window title="Leave a Comment:" className="leave-comment-window">
|
<Window title="Leave a Comment:" className="leave-comment-window">
|
||||||
<textarea name="comment-box" id="comment-text-field"></textarea>
|
<textarea name="comment-box" id="comment-text-field"></textarea>
|
||||||
<Button variant="contained" endIcon={<SendIcon/>}>Submit</Button>
|
<Button variant="contained" endIcon={<SendIcon/>}>Submit</Button>
|
||||||
@ -117,11 +111,6 @@ function TitleAndComments(stats: CreatorAndReviewStatus) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MapInfo {
|
|
||||||
assetid: SubmissionInfo["AssetID"],
|
|
||||||
status: SubmissionStatus,
|
|
||||||
comments: Comment[]
|
|
||||||
}
|
|
||||||
export default function MapInfoPage(info: MapInfo) {
|
export default function MapInfoPage(info: MapInfo) {
|
||||||
return (
|
return (
|
||||||
<section className="review-section">
|
<section className="review-section">
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
@forward "./page/rating_window.scss";
|
@forward "./page/rating_window.scss";
|
||||||
@forward "./page/leave_comment_window.scss";
|
@forward "./page/leave_comment_window.scss";
|
||||||
@forward "./page/review_status.scss";
|
@forward "./page/review_status.scss";
|
||||||
|
@forward "./page/map.scss";
|
||||||
|
|
||||||
@use "../../../globals.scss";
|
@use "../../../globals.scss";
|
||||||
|
|
||||||
@ -15,36 +16,4 @@
|
|||||||
|
|
||||||
.by-creator {
|
.by-creator {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
|
||||||
|
|
||||||
.review-info {
|
|
||||||
width: 650px;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
p, h1 {
|
|
||||||
color: #1e1e1e;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font: {
|
|
||||||
weight: 500;
|
|
||||||
size: 1.8rem
|
|
||||||
};
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #008fd6;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -5,6 +5,11 @@ $comments-size: 60px;
|
|||||||
gap: 25px;
|
gap: 25px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.no-comments {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.commenter {
|
.commenter {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: $comments-size;
|
height: $comments-size;
|
||||||
@ -13,7 +18,6 @@ $comments-size: 60px;
|
|||||||
&[data-highlighted="true"] {
|
&[data-highlighted="true"] {
|
||||||
background-color: #ffffd7;
|
background-color: #ffffd7;
|
||||||
}
|
}
|
||||||
|
|
||||||
> img {
|
> img {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
@ -24,13 +28,11 @@ $comments-size: 60px;
|
|||||||
size: 1.3em;
|
size: 1.3em;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
font-size: .8em;
|
font-size: .8em;
|
||||||
margin: 0 0 0 5px;
|
margin: 0 0 0 5px;
|
||||||
color: #646464
|
color: #646464
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
display: grid;
|
display: grid;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
15
web/src/app/map/[map_name]/styles/page/map.scss
Normal file
15
web/src/app/map/[map_name]/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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,34 @@
|
|||||||
@use "../../../../globals.scss";
|
@use "../../../../globals.scss";
|
||||||
|
|
||||||
|
.review-info {
|
||||||
|
width: 650px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
p, h1 {
|
||||||
|
color: #1e1e1e;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font: {
|
||||||
|
weight: 500;
|
||||||
|
size: 1.8rem
|
||||||
|
};
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #008fd6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.review-section {
|
.review-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 50px;
|
gap: 50px;
|
||||||
@ -12,9 +41,8 @@
|
|||||||
gap: 25px;
|
gap: 25px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@include globals.border-with-radius;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 350px;
|
height: 350px;
|
||||||
object-fit: contain
|
object-fit: contain
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,37 +3,37 @@ const FALLBACK_IMAGE = ""
|
|||||||
type thumbsizes = "420" | "720"
|
type thumbsizes = "420" | "720"
|
||||||
type thumbsize<S extends thumbsizes> = `${S}x${S}`
|
type thumbsize<S extends thumbsizes> = `${S}x${S}`
|
||||||
|
|
||||||
async function RoproxyParse(api: Response): Promise<string> {
|
function Parse(json: any): string {
|
||||||
if (api.ok) {
|
if (json.errors) {
|
||||||
const json = await api.json()
|
console.warn(json.errors)
|
||||||
if (json.errors) {
|
return FALLBACK_IMAGE
|
||||||
console.warn(json.errors)
|
}
|
||||||
return FALLBACK_IMAGE
|
if (json.data) {
|
||||||
}
|
const data = json.data[0]
|
||||||
if (json.data) {
|
if (!data) { //For whatever reason roblox will sometimes return an empty array instead of an error message
|
||||||
const data = json.data[0]
|
console.warn("Roblox gave us no data,", data)
|
||||||
if (!data) { //For whatever reason roblox will sometimes return an empty array instead of an error message
|
return FALLBACK_IMAGE
|
||||||
console.warn("Roblox gave us no data,", json)
|
}
|
||||||
return FALLBACK_IMAGE
|
if (data.state === "Completed") {
|
||||||
}
|
return data.imageUrl
|
||||||
if (data.state === "Completed") {
|
}
|
||||||
return data.imageUrl
|
console.warn(data)
|
||||||
}
|
return FALLBACK_IMAGE
|
||||||
console.warn(data)
|
|
||||||
return FALLBACK_IMAGE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(api)
|
|
||||||
return FALLBACK_IMAGE
|
return FALLBACK_IMAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function AvatarHeadshot<S extends thumbsizes>(userid: number, size: thumbsize<S>): Promise<string> {
|
async function AvatarHeadshot<S extends thumbsizes>(userid: number, size: thumbsize<S>): Promise<string> {
|
||||||
const avatarthumb_api = await fetch(`https://thumbnails.roproxy.com/v1/users/avatar-headshot?userIds=${userid}&size=${size}&format=Png&isCircular=false`)
|
const avatarthumb_api = fetch(`https://thumbnails.roproxy.com/v1/users/avatar-headshot?userIds=${userid}&size=${size}&format=Png&isCircular=false`)
|
||||||
return RoproxyParse(avatarthumb_api)
|
return avatarthumb_api.then(res => res.json()).then(json => Parse(json))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function AssetImage<S extends thumbsizes>(assetid: number, size: thumbsize<S>): Promise<string> {
|
async function AssetImage<S extends thumbsizes>(assetid: number, size: thumbsize<S>): Promise<string> {
|
||||||
const avatarthumb_api = await fetch(`https://thumbnails.roblox.com/v1/assets?assetIds=${assetid}&returnPolicy=PlaceHolder&size=${size}&format=Png&isCircular=false`)
|
const avatarthumb_api = fetch(`https://thumbnails.roblox.com/v1/assets?assetIds=${assetid}&returnPolicy=PlaceHolder&size=${size}&format=Png&isCircular=false`)
|
||||||
return RoproxyParse(avatarthumb_api)
|
return avatarthumb_api.then(res => res.json()).then(json => Parse(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
AvatarHeadshot,
|
||||||
|
AssetImage
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
export const enum SubmissionStatus {
|
const enum SubmissionStatus {
|
||||||
Published,
|
Published,
|
||||||
Rejected,
|
Rejected,
|
||||||
Publishing,
|
Publishing,
|
||||||
@ -10,7 +10,7 @@ export const enum SubmissionStatus {
|
|||||||
UnderConstruction
|
UnderConstruction
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubmissionInfo {
|
interface SubmissionInfo {
|
||||||
readonly ID: number,
|
readonly ID: number,
|
||||||
readonly DisplayName: string,
|
readonly DisplayName: string,
|
||||||
readonly Creator: string,
|
readonly Creator: string,
|
||||||
@ -22,4 +22,46 @@ export interface SubmissionInfo {
|
|||||||
readonly Completed: boolean,
|
readonly Completed: boolean,
|
||||||
readonly TargetAssetID: number,
|
readonly TargetAssetID: number,
|
||||||
readonly StatusID: SubmissionStatus
|
readonly StatusID: SubmissionStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubmissionStatusToString(submission_status: SubmissionStatus): string {
|
||||||
|
let Review: string
|
||||||
|
switch (submission_status) {
|
||||||
|
case SubmissionStatus.Published:
|
||||||
|
Review = "PUBLISHED"
|
||||||
|
break
|
||||||
|
case SubmissionStatus.Rejected:
|
||||||
|
Review = "REJECTED"
|
||||||
|
break
|
||||||
|
case SubmissionStatus.Publishing:
|
||||||
|
Review = "PUBLISHING"
|
||||||
|
break
|
||||||
|
case SubmissionStatus.Validated:
|
||||||
|
Review = "VALIDATED"
|
||||||
|
break
|
||||||
|
case SubmissionStatus.Validating:
|
||||||
|
Review = "VALIDATING"
|
||||||
|
break
|
||||||
|
case SubmissionStatus.Accepted:
|
||||||
|
Review = "ACCEPTED"
|
||||||
|
break
|
||||||
|
case SubmissionStatus.ChangesRequested:
|
||||||
|
Review = "CHANGES REQUESTED"
|
||||||
|
break
|
||||||
|
case SubmissionStatus.Submitted:
|
||||||
|
Review = "SUBMITTED"
|
||||||
|
break
|
||||||
|
case SubmissionStatus.UnderConstruction:
|
||||||
|
Review = "UNDER CONSTRUCTION"
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
Review = "UNKNOWN"
|
||||||
|
}
|
||||||
|
return Review
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
SubmissionStatus,
|
||||||
|
SubmissionStatusToString,
|
||||||
|
type SubmissionInfo
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user