web folder (dont use this code)

This commit is contained in:
rhpidfyre 2024-12-09 21:14:54 -05:00
parent fe6a88a479
commit 6adf9a8977
21 changed files with 739 additions and 0 deletions

42
web/.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
bun.lockb
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

0
web/README.md Normal file
View File

7
web/next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

27
web/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "bhop-website",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.1.10",
"@mui/material": "^6.1.10",
"next": "15.0.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sass": "^1.82.0"
},
"devDependencies": {
"@types/node": "^20.17.9",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"typescript": "^5.7.2"
}
}

View File

@ -0,0 +1,31 @@
import Link from "next/link"
import "./styles/header.scss"
interface HeaderButton {
name: string,
href: string
}
function HeaderButton(header: HeaderButton) {
return (
<Link href={header.href}>
<button>{header.name}</button>
</Link>
)
}
export default function Header() {
return (
<header className="header-bar">
<nav className="left">
<HeaderButton name="Maps" href=""/>
</nav>
<nav className="right">
<HeaderButton name="Home" href=""/>
<HeaderButton name="Need" href=""/>
<HeaderButton name="Menu" href=""/>
<HeaderButton name="Items" href=""/>
</nav>
</header>
)
}

View File

@ -0,0 +1,38 @@
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
width: 100vw;
height: 60px;
background: rgb(59,64,70);
background: linear-gradient(180deg, #363b40 0%, #353a40 100%);
button {
background-color: transparent;
border: 0;
color: white;
}
.left {
margin-left: 15px;
button {
font-size: 1.2rem;
}
}
.right {
display: flex;
gap: 7px;
margin-right: 50px;
button {
font-size: 1rem;
color: rgb(180,180,180);
&:hover {
color: white
}
}
}
}

26
web/src/app/globals.scss Normal file
View File

@ -0,0 +1,26 @@
$review-border-color: #c8c8c8;
$review-border: 1px solid $review-border-color;
@mixin border-with-radius {
border: $review-border {
radius: 5px;
}
}
:root {
color-scheme: light;
}
body {
font-family: -apple-system, "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla";
box-sizing: border-box;
margin: 0;
}
button {
cursor: pointer;
}
a:active, a:link, a:hover {
text-decoration: none;
}

9
web/src/app/layout.tsx Normal file
View File

@ -0,0 +1,9 @@
import "./globals.scss";
export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

View File

@ -0,0 +1,34 @@
import { SubmissionStatus } from "@/app/ts/Submission";
import MapInfoPage from "./page";
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"
}
]
return (
<>
<Header/>
<main className="map-page-main">
<MapInfoPage assetid={14783300138} status={SubmissionStatus.Accepted} comments={placeholder_Comment}/>
</main>
</>
)
}

View File

@ -0,0 +1,132 @@
import { SubmissionStatus, type SubmissionInfo } from "@/app/ts/Submission";
import { useState, type ReactNode } from "react";
import { Rating, Button } from "@mui/material";
import { AssetImage } from "@/app/ts/Roblox";
import SendIcon from '@mui/icons-material/Send';
import "./styles/page.scss";
interface Window {
className: string,
title: string,
children: ReactNode
}
function Window(window: Window) {
return (
<section className={window.className}>
<header>
<p>{window.title}</p>
</header>
<main>{window.children}</main>
</section>
)
}
interface AssetID {
id: SubmissionInfo["AssetID"]
}
async function ImageAndRatings(asset: AssetID) {
const [assetImage, setAssetImage] = useState(null);
const asset_image = await AssetImage(asset.id, "420x420")
return (
<aside className="review-area">
<section className="rating">
<img src={asset_image} alt="Map Image"/>
</section>
<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>
</aside>
)
}
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"
return (
<div className="commenter" data-highlighted={IsBhopMaptest}>
<img src={placeHolder} 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>
);
}
interface CreatorAndReviewStatus {
creator: SubmissionInfo["DisplayName"],
review: SubmissionInfo["StatusID"],
comments: Comment[]
}
function TitleAndComments(stats: CreatorAndReviewStatus) {
//TODO: switch case this for matching the enums
return (
<main className="review-info">
<div>
<h1>bhop_quaternions</h1>
<aside data-review-status={stats.review} className="review-status">
<p>ACCEPTED</p>
</aside>
</div>
<p className="by-creator">by <a href="" target="_blank">{stats.creator}</a></p>
<span className="spacer"></span>
<main className="comments">
{stats.comments.map(comment => (
<Comment name={comment.name} date={comment.date} comment={comment.comment}/>
))}
</main>
<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>
</Window>
</main>
)
}
interface MapInfo {
assetid: SubmissionInfo["AssetID"],
status: SubmissionStatus,
comments: Comment[]
}
export default function MapInfoPage(info: MapInfo) {
return (
<section className="review-section">
<ImageAndRatings id={info.assetid}/>
<TitleAndComments creator="Quaternions" review={info.status} comments={info.comments}/>
</section>
)
}

View File

@ -0,0 +1,5 @@
.map-page-main {
display: flex;
justify-content: center;
width: 100vw;
}

View File

@ -0,0 +1,50 @@
@forward "./page/comments.scss";
@forward "./page/review.scss";
@forward "./page/rating_window.scss";
@forward "./page/leave_comment_window.scss";
@forward "./page/review_status.scss";
@use "../../../globals.scss";
.spacer {
display: block;
width: 100%;
height: 1px;
background-color: globals.$review-border-color;
}
.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

@ -0,0 +1,47 @@
$comments-size: 60px;
.comments {
display: grid;
gap: 25px;
margin-top: 20px;
.commenter {
display: flex;
height: $comments-size;
//BhopMaptest comment
&[data-highlighted="true"] {
background-color: #ffffd7;
}
> 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;
}
}
}
}

View File

@ -0,0 +1,55 @@
@use "../../../../globals.scss";
#comment-text-field {
@include globals.border-with-radius;
resize: none;
width: 100%;
height: 100px;
}
.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: #f5f5f5;
border-bottom: globals.$review-border;
height: 45px;
p {
font-weight: bold;
margin: 0 0 0 20px;
}
}
main {
padding: 20px;
button {
margin-top: 9px;
}
}
}

View File

@ -0,0 +1,44 @@
@use "../../../../globals.scss";
.rating-window {
@include globals.border-with-radius;
width: 100%;
height: 225px;
.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: #f5f5f5;
border-bottom: globals.$review-border;
height: 45px;
p {
font-weight: bold;
margin: 0 0 0 20px;
}
}
main {
display: grid;
place-items: center;
}
}

View File

@ -0,0 +1,20 @@
@use "../../../../globals.scss";
.review-section {
display: flex;
gap: 50px;
margin-top: 20px;
}
.review-area {
display: grid;
justify-content: center;
gap: 25px;
img {
@include globals.border-with-radius;
width: 100%;
height: 350px;
object-fit: contain
}
}

View File

@ -0,0 +1,73 @@
$Published: "0";
$Rejected: "1";
$Publishing: "2";
$Validated: "3";
$Validating: "4";
$Accepted: "5";
$ChangesRequested: "6";
$Submitted: "7";
$UnderConstruction: "8";
.review-status {
border-radius: 5px;
p {
margin: 3px 25px 3px 25px;
font-weight: bold;
}
&[data-review-status="#{$Published}"] {
background-color: orange;
p {
color: white;
}
}
&[data-review-status="#{$Rejected}"] {
background-color: orange;
p {
color: white;
}
}
&[data-review-status="#{$Publishing}"] {
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;
}
}
}

7
web/src/app/page.tsx Normal file
View File

@ -0,0 +1,7 @@
import Header from "./_components/header";
export default function Home() {
return (
<Header/>
);
}

39
web/src/app/ts/Roblox.ts Normal file
View File

@ -0,0 +1,39 @@
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
}
}
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)
}
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)
}

View File

@ -0,0 +1,25 @@
export const enum SubmissionStatus {
Published,
Rejected,
Publishing,
Validated,
Validating,
Accepted,
ChangesRequested,
Submitted,
UnderConstruction
}
export interface SubmissionInfo {
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 Completed: boolean,
readonly TargetAssetID: number,
readonly StatusID: SubmissionStatus
}

28
web/tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"noImplicitAny": true,
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}