From f84f95fcc28fa7fa3aa61cfc3220ce4235d83e30 Mon Sep 17 00:00:00 2001 From: ic3w0lf Date: Fri, 27 Jun 2025 22:28:33 -0600 Subject: [PATCH] Added titles, some buttons, made submitter display usernames instead of userids, quick links Closes #162 Closes #170 --- web/next.config.ts | 5 +- web/src/app/_components/header.tsx | 54 ++++++++++- .../app/_components/review/CopyableField.tsx | 17 +++- web/src/app/_components/review/ReviewItem.tsx | 91 ++++++++++++++++--- .../_components/review/ReviewItemHeader.tsx | 8 +- web/src/app/admin-submit/page.tsx | 3 + web/src/app/layout.tsx | 3 +- web/src/app/mapfixes/[mapfixId]/page.tsx | 5 +- web/src/app/mapfixes/page.tsx | 3 + web/src/app/maps/[mapId]/fix/page.tsx | 3 + web/src/app/maps/[mapId]/page.tsx | 5 +- web/src/app/maps/page.tsx | 5 + web/src/app/operations/[operationId]/page.tsx | 4 +- web/src/app/page.tsx | 3 + .../app/submissions/[submissionId]/page.tsx | 3 + web/src/app/submissions/page.tsx | 5 +- web/src/app/submit/page.tsx | 3 + .../app/thumbnails/asset/[assetId]/route.tsx | 3 +- 18 files changed, 190 insertions(+), 33 deletions(-) diff --git a/web/next.config.ts b/web/next.config.ts index d68a004..7c674d8 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -7,10 +7,7 @@ const nextConfig: NextConfig = { remotePatterns: [ { protocol: "https", - hostname: "tr.rbxcdn.com", - pathname: "/**", - port: "", - search: "", + hostname: "**.rbxcdn.com", }, ], }, diff --git a/web/src/app/_components/header.tsx b/web/src/app/_components/header.tsx index 0eac6b4..801e954 100644 --- a/web/src/app/_components/header.tsx +++ b/web/src/app/_components/header.tsx @@ -21,6 +21,7 @@ import ListItemButton from "@mui/material/ListItemButton"; import ListItemText from "@mui/material/ListItemText"; import useMediaQuery from "@mui/material/useMediaQuery"; import { useTheme } from "@mui/material/styles"; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; interface HeaderButton { name: string; @@ -28,6 +29,7 @@ interface HeaderButton { } const navItems: HeaderButton[] = [ + { name: "Home", href: "/" }, { name: "Submissions", href: "/submissions" }, { name: "Mapfixes", href: "/mapfixes" }, { name: "Maps", href: "/maps" }, @@ -54,6 +56,7 @@ export default function Header() { const [valid, setValid] = useState(false); const [user, setUser] = useState(null); const [anchorEl, setAnchorEl] = useState(null); + const [quickLinksAnchor, setQuickLinksAnchor] = useState(null); const handleMenuOpen = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -67,6 +70,13 @@ export default function Header() { setMobileOpen(!mobileOpen); }; + const handleQuickLinksOpen = (event: React.MouseEvent) => { + setQuickLinksAnchor(event.currentTarget); + }; + const handleQuickLinksClose = () => { + setQuickLinksAnchor(null); + }; + useEffect(() => { async function getLoginInfo() { try { @@ -129,6 +139,15 @@ export default function Header() { ); + const quickLinks = [ + { name: "Bhop", href: "https://www.roblox.com/games/5315046213" }, + { name: "Bhop Maptest", href: "https://www.roblox.com/games/517201717" }, + { name: "Surf", href: "https://www.roblox.com/games/5315066937" }, + { name: "Surf Maptest", href: "https://www.roblox.com/games/517206177" }, + { name: "Fly Trials", href: "https://www.roblox.com/games/12591611759" }, + { name: "Fly Trials Maptest", href: "https://www.roblox.com/games/12724901535" }, + ]; + return ( @@ -146,10 +165,43 @@ export default function Header() { {/* Desktop navigation */} {!isMobile && ( - + {navItems.map((item) => ( ))} + {/* Push quick links to the right */} + {/* Quick Links Dropdown */} + + + + {quickLinks.map(link => ( + + {link.name} + + ))} + + )} diff --git a/web/src/app/_components/review/CopyableField.tsx b/web/src/app/_components/review/CopyableField.tsx index ae0ebe5..e683701 100644 --- a/web/src/app/_components/review/CopyableField.tsx +++ b/web/src/app/_components/review/CopyableField.tsx @@ -6,13 +6,15 @@ interface CopyableFieldProps { value: string | number | null | undefined; onCopy: (value: string) => void; placeholderText?: string; + link?: string; // Optional link prop } export const CopyableField = ({ label, value, onCopy, - placeholderText = "Not assigned" + placeholderText = "Not assigned", + link }: CopyableFieldProps) => { const displayValue = value?.toString() || placeholderText; @@ -20,7 +22,18 @@ export const CopyableField = ({ <> {label} - {displayValue} + {link ? ( + + {displayValue} + + ) : ( + {displayValue} + )} {value && ( (null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!submitterId) return; + + const fetchUserName = async () => { + try { + setLoading(true); + // Use the internal proxy API route + const response = await fetch(`/proxy/users/${submitterId}`); + if (!response.ok) { + throw new Error('Failed to fetch user'); + } + const data = await response.json(); + setName(`@${data.name}`); + } catch (error) { + console.error("Error fetching user name:", error); + setName(String(submitterId)); // Fallback to ID on error + } finally { + setLoading(false); + } + }; + + fetchUserName(); + }, [submitterId]); + + if (loading) { + return Loading...; + } + + return ( + + + + {name || submitterId} + + + + + ); +} // Define a field configuration for specific types interface FieldConfig { @@ -34,14 +82,14 @@ export function ReviewItem({ if (isSubmission) { // Fields for Submission fields = [ - { key: 'Submitter', label: 'Submitter ID' }, + { key: 'Submitter', label: 'Submitter' }, { key: 'AssetID', label: 'Asset ID' }, { key: 'UploadedAssetID', label: 'Uploaded Asset ID' }, ]; } else if (isMapfix) { // Fields for Mapfix fields = [ - { key: 'Submitter', label: 'Submitter ID' }, + { key: 'Submitter', label: 'Submitter' }, { key: 'AssetID', label: 'Asset ID' }, { key: 'TargetAssetID', label: 'Target Asset ID' }, ]; @@ -58,16 +106,33 @@ export function ReviewItem({ {/* Item Details */} - {fields.map((field) => ( - - - - ))} + {fields.map((field) => { + const fieldValue = (item as never)[field.key]; + + if (field.key === 'Submitter') { + return ( + + {field.label} + + + ); + } + + const displayValue = fieldValue === 0 || fieldValue == null ? 'N/A' : fieldValue; + const isAssetId = field.key.includes('AssetID') && fieldValue !== 0 && fieldValue != null; + + return ( + + + + ); + })} {/* Description Section */} diff --git a/web/src/app/_components/review/ReviewItemHeader.tsx b/web/src/app/_components/review/ReviewItemHeader.tsx index f04cf6c..2e2a6e1 100644 --- a/web/src/app/_components/review/ReviewItemHeader.tsx +++ b/web/src/app/_components/review/ReviewItemHeader.tsx @@ -1,4 +1,4 @@ -import {Typography, Box, Avatar, keyframes} from "@mui/material"; +import {Typography, Box, keyframes} from "@mui/material"; import { StatusChip } from "@/app/_components/statusChip"; import { SubmissionStatus } from "@/app/ts/Submission"; import { MapfixStatus } from "@/app/ts/Mapfix"; @@ -13,7 +13,7 @@ interface ReviewItemHeaderProps { submitterId: number; } -export const ReviewItemHeader = ({ displayName, statusId, creator, submitterId }: ReviewItemHeaderProps) => { +export const ReviewItemHeader = ({ displayName, statusId, creator }: ReviewItemHeaderProps) => { const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting]); const pulse = keyframes` @@ -56,10 +56,6 @@ export const ReviewItemHeader = ({ displayName, statusId, creator, submitterId } - by {creator || "Unknown Creator"} diff --git a/web/src/app/admin-submit/page.tsx b/web/src/app/admin-submit/page.tsx index b04a0b9..d76b11c 100644 --- a/web/src/app/admin-submit/page.tsx +++ b/web/src/app/admin-submit/page.tsx @@ -6,6 +6,7 @@ import GameSelection from "./_game"; import SendIcon from '@mui/icons-material/Send'; import Webpage from "@/app/_components/webpage" import React, { useState } from "react"; +import {useTitle} from "@/app/hooks/useTitle"; import "./(styles)/page.scss" @@ -20,6 +21,8 @@ interface IdResponse { } export default function SubmissionInfoPage() { + useTitle("Admin Submit"); + const [game, setGame] = useState(1); const handleSubmit = async (event: React.FormEvent) => { diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 3fb5ba9..8d4e312 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,4 +1,5 @@ -'use client'; +"use client"; + import "./globals.scss"; import {theme} from "@/app/lib/theme"; import {ThemeProvider} from "@mui/material"; diff --git a/web/src/app/mapfixes/[mapfixId]/page.tsx b/web/src/app/mapfixes/[mapfixId]/page.tsx index 548fa76..bc05191 100644 --- a/web/src/app/mapfixes/[mapfixId]/page.tsx +++ b/web/src/app/mapfixes/[mapfixId]/page.tsx @@ -26,6 +26,7 @@ import {ErrorDisplay} from "@/app/_components/ErrorDisplay"; import ReviewButtons from "@/app/_components/review/ReviewButtons"; import {useReviewData} from "@/app/hooks/useReviewData"; import {MapfixInfo} from "@/app/ts/Mapfix"; +import {useTitle} from "@/app/hooks/useTitle"; interface SnackbarState { open: boolean; @@ -50,7 +51,6 @@ export default function MapfixDetailsPage() { severity }); }; - const handleCloseSnackbar = () => { setSnackbar({ ...snackbar, @@ -74,6 +74,8 @@ export default function MapfixDetailsPage() { }); const mapfix = mapfixData as MapfixInfo; + useTitle(mapfix ? `${mapfix.DisplayName} Mapfix` : 'Loading Mapfix...'); + // Handle review button actions async function handleReviewAction(action: string, mapfixId: number) { try { @@ -179,6 +181,7 @@ export default function MapfixDetailsPage() { /> ); } + return ( diff --git a/web/src/app/mapfixes/page.tsx b/web/src/app/mapfixes/page.tsx index dccdc63..b45446f 100644 --- a/web/src/app/mapfixes/page.tsx +++ b/web/src/app/mapfixes/page.tsx @@ -15,8 +15,11 @@ import { } from "@mui/material"; import Link from "next/link"; import NavigateNextIcon from "@mui/icons-material/NavigateNext"; +import {useTitle} from "@/app/hooks/useTitle"; export default function MapfixInfoPage() { + useTitle("Map Fixes"); + const [mapfixes, setMapfixes] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [isLoading, setIsLoading] = useState(false); diff --git a/web/src/app/maps/[mapId]/fix/page.tsx b/web/src/app/maps/[mapId]/fix/page.tsx index 84c5d47..67156e7 100644 --- a/web/src/app/maps/[mapId]/fix/page.tsx +++ b/web/src/app/maps/[mapId]/fix/page.tsx @@ -19,6 +19,7 @@ import Webpage from "@/app/_components/webpage"; import { useParams } from "next/navigation"; import Link from "next/link"; import {MapInfo} from "@/app/ts/Map"; +import {useTitle} from "@/app/hooks/useTitle"; interface MapfixPayload { AssetID: number; @@ -41,6 +42,8 @@ export default function MapfixInfoPage() { const [isLoading, setIsLoading] = useState(true); const [loadError, setLoadError] = useState(null); + useTitle("Submit Mapfix"); + useEffect(() => { const fetchMapDetails = async () => { try { diff --git a/web/src/app/maps/[mapId]/page.tsx b/web/src/app/maps/[mapId]/page.tsx index 53bb57e..90c66e5 100644 --- a/web/src/app/maps/[mapId]/page.tsx +++ b/web/src/app/maps/[mapId]/page.tsx @@ -32,7 +32,8 @@ import BugReportIcon from "@mui/icons-material/BugReport"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; import DownloadIcon from '@mui/icons-material/Download'; -import { hasRole, RolesConstants } from "@/app/ts/Roles"; +import {hasRole, RolesConstants} from "@/app/ts/Roles"; +import {useTitle} from "@/app/hooks/useTitle"; export default function MapDetails() { const { mapId } = useParams(); @@ -44,6 +45,8 @@ export default function MapDetails() { const [roles, setRoles] = useState(RolesConstants.Empty); const [downloading, setDownloading] = useState(false); + useTitle(map ? `${map.DisplayName}` : 'Loading Map...'); + useEffect(() => { async function getMap() { try { diff --git a/web/src/app/maps/page.tsx b/web/src/app/maps/page.tsx index 737e9c9..1a63514 100644 --- a/web/src/app/maps/page.tsx +++ b/web/src/app/maps/page.tsx @@ -26,6 +26,8 @@ import { import {Search as SearchIcon} from "@mui/icons-material"; import Link from "next/link"; import NavigateNextIcon from "@mui/icons-material/NavigateNext"; +import {useTitle} from "@/app/hooks/useTitle"; +import {thumbnailLoader} from '@/app/lib/thumbnailLoader'; interface Map { ID: number; @@ -36,6 +38,8 @@ interface Map { } export default function MapsPage() { + useTitle("Map Collection"); + const router = useRouter(); const [maps, setMaps] = useState([]); const [loading, setLoading] = useState(true); @@ -259,6 +263,7 @@ export default function MapsPage() { {getGameName(map.GameID)} {map.DisplayName}(null); - + + useTitle(`Operation ${operationId}`); useEffect(() => { if (!operationId) return; diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 0ac6930..0ad41a6 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -15,8 +15,11 @@ import { import Link from "next/link"; import {SubmissionInfo, SubmissionList} from "@/app/ts/Submission"; import {Carousel} from "@/app/_components/carousel"; +import {useTitle} from "@/app/hooks/useTitle"; export default function Home() { + useTitle("Home"); + const [mapfixes, setMapfixes] = useState(null); const [submissions, setSubmissions] = useState(null); const [isLoadingMapfixes, setIsLoadingMapfixes] = useState(false); diff --git a/web/src/app/submissions/[submissionId]/page.tsx b/web/src/app/submissions/[submissionId]/page.tsx index b967c44..5654f7f 100644 --- a/web/src/app/submissions/[submissionId]/page.tsx +++ b/web/src/app/submissions/[submissionId]/page.tsx @@ -25,6 +25,7 @@ import {ErrorDisplay} from "@/app/_components/ErrorDisplay"; import ReviewButtons from "@/app/_components/review/ReviewButtons"; import {useReviewData} from "@/app/hooks/useReviewData"; import {SubmissionInfo} from "@/app/ts/Submission"; +import {useTitle} from "@/app/hooks/useTitle"; interface SnackbarState { open: boolean; @@ -73,6 +74,8 @@ export default function SubmissionDetailsPage() { }); const submission = submissionData as SubmissionInfo; + useTitle(submission ? `${submission.DisplayName} Submission` : 'Loading Submission...'); + // Handle review button actions async function handleReviewAction(action: string, submissionId: number) { try { diff --git a/web/src/app/submissions/page.tsx b/web/src/app/submissions/page.tsx index 4c226f9..877cb43 100644 --- a/web/src/app/submissions/page.tsx +++ b/web/src/app/submissions/page.tsx @@ -1,4 +1,4 @@ -'use client' +"use client" import { useState, useEffect } from "react"; import { SubmissionList } from "../ts/Submission"; @@ -15,8 +15,11 @@ import { } from "@mui/material"; import Link from "next/link"; import NavigateNextIcon from "@mui/icons-material/NavigateNext"; +import {useTitle} from "@/app/hooks/useTitle"; export default function SubmissionInfoPage() { + useTitle("Submissions"); + const [submissions, setSubmissions] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [isLoading, setIsLoading] = useState(false); diff --git a/web/src/app/submit/page.tsx b/web/src/app/submit/page.tsx index 51c2963..5ac4b44 100644 --- a/web/src/app/submit/page.tsx +++ b/web/src/app/submit/page.tsx @@ -18,6 +18,7 @@ import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import Webpage from "@/app/_components/webpage"; import GameSelection from "./_game"; import Link from "next/link"; +import {useTitle} from "@/app/hooks/useTitle"; interface SubmissionPayload { AssetID: number; @@ -27,6 +28,8 @@ interface SubmissionPayload { } export default function SubmitPage() { + useTitle("Submit"); + const [game, setGame] = useState(1); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); diff --git a/web/src/app/thumbnails/asset/[assetId]/route.tsx b/web/src/app/thumbnails/asset/[assetId]/route.tsx index 338de9b..0c76487 100644 --- a/web/src/app/thumbnails/asset/[assetId]/route.tsx +++ b/web/src/app/thumbnails/asset/[assetId]/route.tsx @@ -17,7 +17,7 @@ export async function GET( try { const mediaResponse = await fetch( - `https://publish.roblox.com/v1/assets/${assetId}/media` + `https://publish.roblox.com/v1/assets/${assetId}/media` // NOTE: This allows users to add custom images(their own thumbnail if they'd like) to their maps ); if (mediaResponse.ok) { const mediaData = await mediaResponse.json(); @@ -47,7 +47,6 @@ export async function GET( // Redirect to the actual image URL instead of proxying return NextResponse.redirect(imageUrl); - } catch (err) { return errorImageResponse(500, { message: `Failed to fetch thumbnail URL: ${err}`,