From dcbb2df31962cf545f59c0e02799f87e7e2c8f04 Mon Sep 17 00:00:00 2001 From: itzaname Date: Sat, 7 Jun 2025 16:03:09 -0400 Subject: [PATCH] Rework submission/mapfix list views --- web/src/app/_components/mapCard.tsx | 314 +++++++++++++++++++++++----- web/src/app/layout.tsx | 9 +- web/src/app/lib/theme.tsx | 91 ++++++++ web/src/app/mapfixes/page.tsx | 136 ++++++------ web/src/app/submissions/page.tsx | 121 ++++++----- web/src/app/ts/Mapfix.ts | 2 +- 6 files changed, 496 insertions(+), 177 deletions(-) create mode 100644 web/src/app/lib/theme.tsx diff --git a/web/src/app/_components/mapCard.tsx b/web/src/app/_components/mapCard.tsx index 539c08e..0683aa7 100644 --- a/web/src/app/_components/mapCard.tsx +++ b/web/src/app/_components/mapCard.tsx @@ -1,71 +1,269 @@ import React from "react"; -import Image from "next/image"; -import Link from "next/link"; -import { Rating } from "@mui/material"; +import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Chip, Divider, Typography} from "@mui/material"; +import {Grid} from "@mui/system"; +import {Cancel, CheckCircle, Explore, Pending, Person2} from "@mui/icons-material"; -interface SubmissionCardProps { +interface MapCardProps { displayName: string; assetId: number; authorId: number; author: string; rating: number; id: number; + statusID: number; + gameID: number; + created: number; + type: 'mapfix' | 'submission'; } -export function SubmissionCard(props: SubmissionCardProps) { - return ( - -
-
-
- {/* TODO: Grab image of model */} - {props.displayName} -
-
-
- {props.displayName} -
- -
-
-
-
- {props.author}/ - {props.author} -
-
-
-
-
- - ); -} +const CARD_WIDTH = 270; -export function MapfixCard(props: SubmissionCardProps) { +export function MapCard(props: MapCardProps) { + const StatusChip = ({status}: { status: number }) => { + let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default'; + let icon: JSX.Element = ; + let label: string = 'Unknown'; + + switch (status) { + case 0: + color = 'info'; + icon = ; + label = 'Under Construction'; + break; + case 1: + color = 'warning'; + icon = ; + label = 'Changes Requested'; + break; + case 2: + color = 'info'; + icon = ; + label = 'Submitting'; + break; + case 3: + color = 'info'; + icon = ; + label = 'Submitted'; + break; + case 4: + color = 'warning'; + icon = ; + label = 'Accepted Unvalidated'; + break; + case 5: + color = 'info'; + icon = ; + label = 'Validating'; + break; + case 6: + color = 'success'; + icon = ; + label = 'Validated'; + break; + case 7: + color = 'info'; + icon = ; + label = 'Uploading'; + break; + case 8: + color = 'success'; + icon = ; + label = 'Uploaded'; + break; + case 9: + color = 'error'; + icon = ; + label = 'Rejected'; + break; + case 10: + color = 'success'; + icon = ; + label = 'Released'; + break; + default: + color = 'default'; + icon = ; + label = 'Unknown'; + break; + } + + return ( + + ); + }; return ( - -
-
-
- {/* TODO: Grab image of model */} - {props.displayName} -
-
-
- {props.displayName} -
- -
-
-
-
- {props.author}/ - {props.author} -
-
-
-
-
- - ); + + + + + + + + + + + + + + {props.displayName} + + + + + {props.gameID === 1 ? 'Bhop' : props.gameID === 2 ? 'Surf' : props.gameID === 3 ? 'Fly Trials' : props.gameID === 4 ? 'Deathrun' : 'Unknown'} + + + + + + {props.author} + + + + + + + + + {/*In the future author should be the username of the submitter not the info from the map*/} + {props.author} - {new Date(props.created * 1000).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + })} + + + + + + + + + ) } diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index d524fd0..3fb5ba9 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,9 +1,16 @@ +'use client'; import "./globals.scss"; +import {theme} from "@/app/lib/theme"; +import {ThemeProvider} from "@mui/material"; export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) { return ( - {children} + + + {children} + + ); } \ No newline at end of file diff --git a/web/src/app/lib/theme.tsx b/web/src/app/lib/theme.tsx new file mode 100644 index 0000000..07d46c7 --- /dev/null +++ b/web/src/app/lib/theme.tsx @@ -0,0 +1,91 @@ +import {createTheme} from "@mui/material"; + +export const theme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '#90caf9', + }, + secondary: { + main: '#f48fb1', + }, + background: { + default: '#121212', + paper: '#1e1e1e', + }, + }, + typography: { + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + h5: { + fontWeight: 500, + letterSpacing: '0.5px', + }, + subtitle1: { + fontWeight: 500, + fontSize: '0.95rem', + }, + body2: { + fontSize: '0.875rem', + }, + caption: { + fontSize: '0.75rem', + }, + }, + shape: { + borderRadius: 8, + }, + components: { + MuiCard: { + styleOverrides: { + root: { + borderRadius: 8, + overflow: 'hidden', + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', + transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out', + '&:hover': { + transform: 'translateY(-4px)', + boxShadow: '0 8px 16px rgba(0, 0, 0, 0.2)', + }, + }, + }, + }, + MuiCardMedia: { + styleOverrides: { + root: { + borderBottom: '1px solid rgba(255, 255, 255, 0.1)', + }, + }, + }, + MuiCardContent: { + styleOverrides: { + root: { + padding: 16, + '&:last-child': { + paddingBottom: 16, + }, + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + fontWeight: 500, + }, + }, + }, + MuiDivider: { + styleOverrides: { + root: { + borderColor: 'rgba(255, 255, 255, 0.1)', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + }, + }, + }, +}); \ No newline at end of file diff --git a/web/src/app/mapfixes/page.tsx b/web/src/app/mapfixes/page.tsx index e4674b6..a78ab3a 100644 --- a/web/src/app/mapfixes/page.tsx +++ b/web/src/app/mapfixes/page.tsx @@ -2,69 +2,65 @@ import { useState, useEffect } from "react"; import { MapfixList } from "../ts/Mapfix"; -import { MapfixCard } from "../_components/mapCard"; +import {MapCard} from "../_components/mapCard"; import Webpage from "@/app/_components/webpage"; // TODO: MAKE MAPFIX & SUBMISSIONS USE THE SAME COMPONENTS :angry: (currently too lazy) import "./(styles)/page.scss"; import { ListSortConstants } from "../ts/Sort"; +import {Box, Breadcrumbs, CircularProgress, Pagination, Typography} from "@mui/material"; +import Link from "next/link"; export default function MapfixInfoPage() { const [mapfixes, setMapfixes] = useState(null) const [currentPage, setCurrentPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); const cardsPerPage = 24; // built to fit on a 1920x1080 monitor useEffect(() => { - async function fetchMapfixes() { - const res = await fetch(`/api/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`) + const controller = new AbortController(); + + async function fetchMapFixes() { + setIsLoading(true); + const res = await fetch(`/api/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`, { + signal: controller.signal, + }); if (res.ok) { - setMapfixes(await res.json()) + setMapfixes(await res.json()); } + setIsLoading(false); } - setTimeout(() => { - fetchMapfixes() - }, 50); - }, [currentPage]) + fetchMapFixes(); - if (!mapfixes) { + return () => controller.abort(); // Cleanup to avoid fetch conflicts on rapid page changes + }, [currentPage]); + + if (isLoading || !mapfixes) { return -
- Loading... +
+ + + + Loading submissions... + +
- + ; } const totalPages = Math.ceil(mapfixes.Total / cardsPerPage); - - const currentCards = mapfixes.Mapfixes.slice( - (currentPage - 1) * cardsPerPage, - currentPage * cardsPerPage - ); - - const nextPage = () => { - if (currentPage < totalPages) { - setCurrentPage(currentPage + 1); - } - }; - - const prevPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; - - if (mapfixes.Total == 0) { - return -
- Mapfixes list is empty. -
-
- } + const currentCards = mapfixes.Mapfixes; return ( - // TODO: Add filter settings & searchbar & page selector
-
- {Array.from({ length: totalPages }).map((_, index) => ( - setCurrentPage(index+1)} - > - ))} -
-
- - - Page {currentPage} of {totalPages} - - -
-
- {currentCards.map((mapfix) => ( - + + Home + + Mapfixes + +
+ {currentCards.map((submission) => ( + ))}
+
+ setCurrentPage(page)} + variant="outlined" + shape="rounded" + /> +
) diff --git a/web/src/app/submissions/page.tsx b/web/src/app/submissions/page.tsx index bd67b33..b480c29 100644 --- a/web/src/app/submissions/page.tsx +++ b/web/src/app/submissions/page.tsx @@ -1,68 +1,72 @@ 'use client' -import { useState, useEffect } from "react"; -import { SubmissionList } from "../ts/Submission"; -import { SubmissionCard } from "../_components/mapCard"; +import {useState, useEffect} from "react"; +import {SubmissionList} from "../ts/Submission"; +import {MapCard} from "../_components/mapCard"; import Webpage from "@/app/_components/webpage"; import "./(styles)/page.scss"; -import { ListSortConstants } from "../ts/Sort"; +import {ListSortConstants} from "../ts/Sort"; +import {Breadcrumbs, Pagination, Typography, CircularProgress, Box} from "@mui/material"; +import Link from "next/link"; export default function SubmissionInfoPage() { - const [submissions, setSubmissions] = useState(null) + const [submissions, setSubmissions] = useState(null); const [currentPage, setCurrentPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); const cardsPerPage = 24; // built to fit on a 1920x1080 monitor useEffect(() => { + const controller = new AbortController(); + async function fetchSubmissions() { - const res = await fetch(`/api/submissions?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`) + setIsLoading(true); + const res = await fetch(`/api/submissions?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`, { + signal: controller.signal, + }); if (res.ok) { - setSubmissions(await res.json()) + setSubmissions(await res.json()); } + setIsLoading(false); } - setTimeout(() => { - fetchSubmissions() - }, 50); - }, [currentPage]) + fetchSubmissions(); - if (!submissions) { + return () => controller.abort(); // Cleanup to avoid fetch conflicts on rapid page changes + }, [currentPage]); + + if (isLoading || !submissions) { return -
- Loading... +
+ + + + Loading submissions... + +
- + ; } const totalPages = Math.ceil(submissions.Total / cardsPerPage); + const currentCards = submissions.Submissions; - const currentCards = submissions.Submissions.slice( - (currentPage - 1) * cardsPerPage, - currentPage * cardsPerPage - ); - - const nextPage = () => { - if (currentPage < totalPages) { - setCurrentPage(currentPage + 1); - } - }; - - const prevPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; - - if (submissions.Total == 0) { + if (submissions.Total === 0) { return
Submissions list is empty.
-
+ ; } return ( - // TODO: Add filter settings & searchbar & page selector
-
- {Array.from({ length: totalPages }).map((_, index) => ( - setCurrentPage(index+1)} - > - ))} -
-
- - - Page {currentPage} of {totalPages} - - -
-
+ + + Home + + Submissions + +
{currentCards.map((submission) => ( - ))}
+
+ setCurrentPage(page)} + variant="outlined" + shape="rounded" + /> +
) diff --git a/web/src/app/ts/Mapfix.ts b/web/src/app/ts/Mapfix.ts index c4a9bce..a09ed2b 100644 --- a/web/src/app/ts/Mapfix.ts +++ b/web/src/app/ts/Mapfix.ts @@ -17,7 +17,7 @@ interface MapfixInfo { readonly DisplayName: string, readonly Creator: string, readonly GameID: number, - readonly Date: number, + readonly CreatedAt: number, readonly Submitter: number, readonly AssetID: number, readonly AssetVersion: number,