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";
interface Component {
}
export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) {
return (
<html lang="en">

View File

@ -5,29 +5,29 @@ import Header from "@/app/_components/header";
import "./styles/layout.scss";
export default function MapPage() {
const placeholder_Comment = [
{
comment: "This map has been accepted and is in the game.",
date: "on Dec 8 '24 at 18:46",
name: "BhopMaptest"
},
{
comment: "This map is so mid...",
date: "on Dec 8 '24 at 18:46",
name: "vmsize"
},
{
comment: "I prefer strafe client",
date: "on Dec 8 '24 at 18:46",
name: "Quaternions"
}
]
// const placeholder_Comments = [
// {
// comment: "This map has been accepted and is in the game.",
// date: "on Dec 8 '24 at 18:46",
// name: "BhopMaptest"
// },
// {
// comment: "This map is so mid...",
// date: "on Dec 8 '24 at 18:46",
// name: "vmsize"
// },
// {
// comment: "I prefer strafe client",
// date: "on Dec 8 '24 at 18:46",
// name: "Quaternions"
// }
// ]
return (
<>
<Header/>
<main className="map-page-main">
<MapInfoPage assetid={14783300138} status={SubmissionStatus.Accepted} comments={placeholder_Comment}/>
<MapInfoPage assetid={14783300138} status={SubmissionStatus.Accepted} comments={[]}/>
</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";
import { useState, type ReactNode } from "react";
"use client"
import { SubmissionStatus, SubmissionStatusToString, type SubmissionInfo } from "@/app/ts/Submission";
import { MapImage, type AssetID } from "./map";
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 Link from "next/link";
import "./styles/page.scss";
@ -11,6 +14,23 @@ interface Window {
title: string,
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) {
return (
<section className={window.className}>
@ -22,17 +42,11 @@ function Window(window: Window) {
)
}
interface AssetID {
id: SubmissionInfo["AssetID"]
}
async function ImageAndRatings(asset: AssetID) {
const [assetImage, setAssetImage] = useState(null);
const asset_image = await AssetImage(asset.id, "420x420")
function ImageAndRatings(asset: AssetID) {
return (
<aside className="review-area">
<section className="rating">
<img src={asset_image} alt="Map Image"/>
<section className="map-image-area">
<MapImage id={asset.id}/>
</section>
<Window className="rating-window" title="Rating">
<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) {
let placeHolder;
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"
const IsBhopMaptest = comment.name == "BhopMaptest" //Highlighted commenter
return (
<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">
<header>
<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) {
//TODO: switch case this for matching the enums
return (
const Review = SubmissionStatusToString(stats.review)
return (
<main className="review-info">
<div>
<h1>bhop_quaternions</h1>
<aside data-review-status={stats.review} className="review-status">
<p>ACCEPTED</p>
<p>{Review}</p>
</aside>
</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>
<main className="comments">
{stats.comments.map(comment => (
<Comment name={comment.name} date={comment.date} comment={comment.comment}/>
))}
</main>
<section className="comments">
{stats.comments.length===0 && <p className="no-comments">There are no comments.</p> || stats.comments.map(comment => (
<Comment key={comment.name} name={comment.name} date={comment.date} comment={comment.comment}/>
))}
</section>
<Window title="Leave a Comment:" className="leave-comment-window">
<textarea name="comment-box" id="comment-text-field"></textarea>
<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) {
return (
<section className="review-section">

View File

@ -3,6 +3,7 @@
@forward "./page/rating_window.scss";
@forward "./page/leave_comment_window.scss";
@forward "./page/review_status.scss";
@forward "./page/map.scss";
@use "../../../globals.scss";
@ -15,36 +16,4 @@
.by-creator {
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;
margin-top: 20px;
.no-comments {
text-align: center;
margin: 0;
}
.commenter {
display: flex;
height: $comments-size;
@ -13,7 +18,6 @@ $comments-size: 60px;
&[data-highlighted="true"] {
background-color: #ffffd7;
}
> img {
border-radius: 50%;
}
@ -24,13 +28,11 @@ $comments-size: 60px;
size: 1.3em;
};
}
.date {
font-size: .8em;
margin: 0 0 0 5px;
color: #646464
}
.details {
display: grid;
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";
.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 {
display: flex;
gap: 50px;
@ -12,9 +41,8 @@
gap: 25px;
img {
@include globals.border-with-radius;
width: 100%;
height: 350px;
object-fit: contain
}
}
}

View File

@ -3,37 +3,37 @@ const FALLBACK_IMAGE = ""
type thumbsizes = "420" | "720"
type thumbsize<S extends thumbsizes> = `${S}x${S}`
async function RoproxyParse(api: Response): Promise<string> {
if (api.ok) {
const json = await api.json()
if (json.errors) {
console.warn(json.errors)
return FALLBACK_IMAGE
}
if (json.data) {
const data = json.data[0]
if (!data) { //For whatever reason roblox will sometimes return an empty array instead of an error message
console.warn("Roblox gave us no data,", json)
return FALLBACK_IMAGE
}
if (data.state === "Completed") {
return data.imageUrl
}
console.warn(data)
return FALLBACK_IMAGE
}
function Parse(json: any): string {
if (json.errors) {
console.warn(json.errors)
return FALLBACK_IMAGE
}
if (json.data) {
const data = json.data[0]
if (!data) { //For whatever reason roblox will sometimes return an empty array instead of an error message
console.warn("Roblox gave us no data,", data)
return FALLBACK_IMAGE
}
if (data.state === "Completed") {
return data.imageUrl
}
console.warn(data)
return FALLBACK_IMAGE
}
console.warn(api)
return FALLBACK_IMAGE
}
export 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`)
return RoproxyParse(avatarthumb_api)
async function AvatarHeadshot<S extends thumbsizes>(userid: number, size: thumbsize<S>): Promise<string> {
const avatarthumb_api = fetch(`https://thumbnails.roproxy.com/v1/users/avatar-headshot?userIds=${userid}&size=${size}&format=Png&isCircular=false`)
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> {
const avatarthumb_api = await fetch(`https://thumbnails.roblox.com/v1/assets?assetIds=${assetid}&returnPolicy=PlaceHolder&size=${size}&format=Png&isCircular=false`)
return RoproxyParse(avatarthumb_api)
async function AssetImage<S extends thumbsizes>(assetid: number, size: thumbsize<S>): Promise<string> {
const avatarthumb_api = fetch(`https://thumbnails.roblox.com/v1/assets?assetIds=${assetid}&returnPolicy=PlaceHolder&size=${size}&format=Png&isCircular=false`)
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,
Rejected,
Publishing,
@ -10,7 +10,7 @@ export const enum SubmissionStatus {
UnderConstruction
}
export interface SubmissionInfo {
interface SubmissionInfo {
readonly ID: number,
readonly DisplayName: string,
readonly Creator: string,
@ -22,4 +22,46 @@ export interface SubmissionInfo {
readonly Completed: boolean,
readonly TargetAssetID: number,
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
}