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:
rhpidfyre 2024-12-10 03:53:51 -05:00
parent 37e4e29f04
commit 1a5f58a3b1
10 changed files with 207 additions and 133 deletions

View File

@ -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">

View File

@ -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>
</> </>
) )

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

View File

@ -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">

View File

@ -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;
}
}
} }

View File

@ -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;

View 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;
}
}

View File

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

View File

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

View File

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