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";
|
||||
|
||||
interface Component {
|
||||
|
||||
}
|
||||
export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
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";
|
||||
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">
|
||||
|
@ -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";
|
||||
|
||||
@ -16,35 +17,3 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
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";
|
||||
|
||||
.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,7 +41,6 @@
|
||||
gap: 25px;
|
||||
|
||||
img {
|
||||
@include globals.border-with-radius;
|
||||
width: 100%;
|
||||
height: 350px;
|
||||
object-fit: contain
|
||||
|
@ -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
|
||||
}
|
@ -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,
|
||||
@ -23,3 +23,45 @@ export interface SubmissionInfo {
|
||||
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
|
||||
}
|
Loading…
Reference in New Issue
Block a user