diff --git a/web/src/app/_components/carousel.tsx b/web/src/app/_components/carousel.tsx new file mode 100644 index 0000000..c8c26d2 --- /dev/null +++ b/web/src/app/_components/carousel.tsx @@ -0,0 +1,151 @@ +import {Box, IconButton, Typography} from "@mui/material"; +import {useEffect, useRef, useState} from "react"; +import Link from "next/link"; +import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; +import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; +import {SubmissionInfo} from "@/app/ts/Submission"; +import {MapfixInfo} from "@/app/ts/Mapfix"; + +// Type for the items in the carousel +type CarouselItem = SubmissionInfo | MapfixInfo; + +// Props for the Carousel component +interface CarouselProps { + title: string; + items: T[] | undefined; + renderItem: (item: T) => React.ReactNode; + viewAllLink: string; +} + +export function Carousel({ title, items, renderItem, viewAllLink }: CarouselProps) { + const carouselRef = useRef(null); + const [scrollPosition, setScrollPosition] = useState(0); + const [maxScroll, setMaxScroll] = useState(0); + + const SCROLL_AMOUNT = 300; + + useEffect(() => { + if (carouselRef.current) { + const scrollWidth = carouselRef.current.scrollWidth; + const clientWidth = carouselRef.current.clientWidth; + setMaxScroll(scrollWidth - clientWidth); + } + }, [items]); + + const scroll = (direction: 'left' | 'right'): void => { + if (carouselRef.current) { + const scrollAmount = direction === 'left' ? -SCROLL_AMOUNT : SCROLL_AMOUNT; + + carouselRef.current.scrollBy({ + left: scrollAmount, + behavior: 'smooth' + }); + + setTimeout(() => { + if (carouselRef.current) { + setScrollPosition(carouselRef.current.scrollLeft); + } + }, 300); + } + }; + + useEffect(() => { + const handleScroll = () => { + if (carouselRef.current) { + setScrollPosition(carouselRef.current.scrollLeft); + } + }; + + const ref = carouselRef.current; + if (ref) { + ref.addEventListener('scroll', handleScroll); + return () => ref.removeEventListener('scroll', handleScroll); + } + }, []); + + return ( + + + + {title} + + + + View All → + + + + + + scroll('left')} + > + + + + + {items?.map((item, index) => ( + + {renderItem(item)} + + ))} + + + = maxScroll - 5 ? 'hidden' : 'visible', + }} + onClick={() => scroll('right')} + > + + + + + ); +} \ No newline at end of file diff --git a/web/src/app/_components/header.tsx b/web/src/app/_components/header.tsx index b611533..463d62e 100644 --- a/web/src/app/_components/header.tsx +++ b/web/src/app/_components/header.tsx @@ -3,62 +3,132 @@ import Link from "next/link" import Image from "next/image"; -import "./styles/header.scss" -import { UserInfo } from "@/app/ts/User"; -import { useState, useEffect } from "react"; +import {UserInfo} from "@/app/ts/User"; +import {useState, useEffect} from "react"; + +import AppBar from "@mui/material/AppBar"; +import Toolbar from "@mui/material/Toolbar"; +import Button from "@mui/material/Button"; +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; interface HeaderButton { - name: string, - href: string + name: string; + href: string; } + function HeaderButton(header: HeaderButton) { - return ( - - - - ) + return ( + + ); } export default function Header() { - const handleLoginClick = () => { - window.location.href = "/auth/oauth2/login?redirect=" + window.location.href; - }; + const handleLoginClick = () => { + window.location.href = + "/auth/oauth2/login?redirect=" + window.location.href; + }; - const [valid, setValid] = useState(false) - const [user, setUser] = useState(null) + const [valid, setValid] = useState(false); + const [user, setUser] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); - useEffect(() => { - async function getLoginInfo() { - const [validateData, userData] = await Promise.all([ - fetch("/api/session/validate").then(validateResponse => validateResponse.json()), - fetch("/api/session/user").then(userResponse => userResponse.json()) - ]); - setValid(validateData) - setUser(userData) - } - getLoginInfo() - }, []) + const handleMenuOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; - return ( -
- - -
- ) + const handleMenuClose = () => { + setAnchorEl(null); + }; + + useEffect(() => { + async function getLoginInfo() { + try { + const response = await fetch("/api/session/user"); + + if (!response.ok) { + setValid(false); + setUser(null); + return; + } + + const userData = await response.json(); + const isLoggedIn = userData && 'UserID' in userData; + + setValid(isLoggedIn); + setUser(isLoggedIn ? userData : null); + } catch (error) { + console.error("Error fetching user data:", error); + setValid(false); + setUser(null); + } + } + + getLoginInfo(); + }, []); + + return ( + + + + + + + + + {valid && user && ( + + )} + {valid && user ? ( + + + + + Manage + + + + ) : ( + + )} + + + + ); } diff --git a/web/src/app/_components/mapCard.tsx b/web/src/app/_components/mapCard.tsx index 539c08e..ff46e5d 100644 --- a/web/src/app/_components/mapCard.tsx +++ b/web/src/app/_components/mapCard.tsx @@ -1,71 +1,268 @@ -import React from "react"; -import Image from "next/image"; -import Link from "next/link"; -import { Rating } from "@mui/material"; +import React, {JSX} from "react"; +import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Chip, Divider, Grid, Typography} from "@mui/material"; +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 = 'warning'; + 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 = 'warning'; + icon = ; + label = 'Under Review'; + 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 === 5 ? '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..7eba041 100644 --- a/web/src/app/mapfixes/page.tsx +++ b/web/src/app/mapfixes/page.tsx @@ -2,113 +2,130 @@ 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, Container, 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... -
-
- } - - 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. -
-
- } - - return ( - // TODO: Add filter settings & searchbar & page selector -
-
- {Array.from({ length: totalPages }).map((_, index) => ( - setCurrentPage(index+1)} - > - ))} -
-
- - - Page {currentPage} of {totalPages} - - -
-
- {currentCards.map((mapfix) => ( - - ))} -
+ + + + Loading mapfixes... + +
+
; + } + + const totalPages = Math.ceil(mapfixes.Total / cardsPerPage); + const currentCards = mapfixes.Mapfixes; + + return ( + + +
+ + + Home + + Mapfixes + + + Map Fixes + + + Explore all submitted fixes for maps from the community. + +
+ {currentCards.map((submission) => ( + + ))} +
+ +
+ setCurrentPage(page)} + variant="outlined" + shape="rounded" + /> +
+
+
+
) } diff --git a/web/src/app/maps/page.tsx b/web/src/app/maps/page.tsx index 7e62df3..b93aa66 100644 --- a/web/src/app/maps/page.tsx +++ b/web/src/app/maps/page.tsx @@ -1,60 +1,298 @@ "use client"; +import {useState, useEffect} from "react"; import Image from "next/image"; -import { useState, useEffect } from "react"; +import {useRouter} from "next/navigation"; import Webpage from "@/app/_components/webpage"; - -import "./(styles)/page.scss"; +import { + Box, + Container, + Typography, + Grid, + Card, + CardContent, + CardMedia, + CardActionArea, + TextField, + InputAdornment, + Pagination, + CircularProgress, + FormControl, + InputLabel, + Select, + MenuItem, + SelectChangeEvent, Breadcrumbs +} from "@mui/material"; +import {Search as SearchIcon} from "@mui/icons-material"; +import Link from "next/link"; interface Map { - ID: number; - DisplayName: string; - Creator: string; - GameID: number; - Date: number; + ID: number; + DisplayName: string; + Creator: string; + GameID: number; + Date: number; } -// TODO: should rewrite this entire page, just wanted to get a simple page working. This was written by chatgippity - export default function MapsPage() { - const [maps, setMaps] = useState([]); + const router = useRouter(); + const [maps, setMaps] = useState([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [gameFilter, setGameFilter] = useState("0"); // 0 means "All Maps" + const mapsPerPage = 12; + const requestPageSize = 100; - useEffect(() => { - const fetchMaps = async () => { - const res = await fetch("/api/maps?Page=1&Limit=100"); - const data: Map[] = await res.json(); - setMaps(data); - }; + useEffect(() => { + const fetchMaps = async () => { + // Just send it and load all maps hoping for the best + try { + setLoading(true); + let allMaps: Map[] = []; + let page = 1; + let hasMore = true; - fetchMaps(); - }, []); + while (hasMore) { + const res = await fetch(`/api/maps?Page=${page}&Limit=${requestPageSize}`); + const data: Map[] = await res.json(); + allMaps = [...allMaps, ...data]; + hasMore = data.length === requestPageSize; + page++; + } - const customLoader = ({ src }: { src: string }) => { - return src; - }; + setMaps(allMaps); + } catch (error) { + console.error("Failed to fetch maps:", error); + } finally { + setLoading(false); + } + }; - return ( - -
- {maps.map((map) => ( - - ))} -
-
- ); + fetchMaps(); + }, []); + + const handleGameFilterChange = (event: SelectChangeEvent) => { + setGameFilter(event.target.value); + setCurrentPage(1); + }; + + // Filter maps based on search query and game filter + const filteredMaps = maps.filter(map => { + const matchesSearch = + map.DisplayName.toLowerCase().includes(searchQuery.toLowerCase()) || + map.Creator.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesGameFilter = + gameFilter === "0" || // "All Maps" + map.GameID === parseInt(gameFilter); + + return matchesSearch && matchesGameFilter; + }); + + // Calculate pagination + const totalPages = Math.ceil(filteredMaps.length / mapsPerPage); + const currentMaps = filteredMaps.slice( + (currentPage - 1) * mapsPerPage, + currentPage * mapsPerPage + ); + + const handlePageChange = (_event: React.ChangeEvent, page: number) => { + setCurrentPage(page); + window.scrollTo({top: 0, behavior: 'smooth'}); + }; + + const handleMapClick = (mapId: number) => { + router.push(`/maps/${mapId}`); + }; + + const formatDate = (timestamp: number) => { + return new Date(timestamp * 1000).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + }; + + const getGameName = (gameId: number) => { + switch (gameId) { + case 1: + return "Bhop"; + case 2: + return "Surf"; + case 5: + return "Fly Trials"; + default: + return "Unknown"; + } + }; + + const getGameLabelStyles = (gameId: number) => { + switch (gameId) { + case 1: // Bhop + return { + bgcolor: "info.main", + color: "white", + }; + case 2: // Surf + return { + bgcolor: "success.main", + color: "white", + }; + case 5: // Fly Trials + return { + bgcolor: "warning.main", + color: "white", + }; + default: // Unknown + return { + bgcolor: "grey.500", + color: "white", + }; + } + }; + + return ( + + + + + + Home + + Maps + + + Map Collection + + + Browse all community-created maps or find your favorites + + { + setSearchQuery(e.target.value); + setCurrentPage(1); + }} + InputProps={{ + startAdornment: ( + + + + ), + }} + sx={{mb: 4}} + /> + + {loading ? ( + + + + ) : ( + <> + + + Showing {filteredMaps.length} {filteredMaps.length === 1 ? 'map' : 'maps'} + + + + Filter by Game + + + + + + {currentMaps.map((map) => ( + + + handleMapClick(map.ID)}> + + + {getGameName(map.GameID)} + + {map.DisplayName} + + + + {map.DisplayName} + + + By {map.Creator} + + + Added {formatDate(map.Date)} + + + + + + ))} + + + {totalPages > 1 && ( + + + + )} + + )} + + + + ); } \ No newline at end of file diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index f6de7c1..0ac6930 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,7 +1,228 @@ +'use client' + +import { useState, useEffect } from "react"; +import {MapfixInfo, MapfixList} from "./ts/Mapfix"; +import { MapCard } from "./_components/mapCard"; import Webpage from "./_components/webpage"; +import { ListSortConstants } from "./ts/Sort"; +import { + Box, + Container, + CircularProgress, + Typography, + Paper, +} from "@mui/material"; +import Link from "next/link"; +import {SubmissionInfo, SubmissionList} from "@/app/ts/Submission"; +import {Carousel} from "@/app/_components/carousel"; export default function Home() { + const [mapfixes, setMapfixes] = useState(null); + const [submissions, setSubmissions] = useState(null); + const [isLoadingMapfixes, setIsLoadingMapfixes] = useState(false); + const [isLoadingSubmissions, setIsLoadingSubmissions] = useState(false); + const itemsPerSection: number = 8; // Show more items for the carousel + + useEffect(() => { + const mapfixController = new AbortController(); + const submissionsController = new AbortController(); + + async function fetchMapFixes(): Promise { + setIsLoadingMapfixes(true); + try { + const res = await fetch(`/api/mapfixes?Page=1&Limit=${itemsPerSection}&Sort=${ListSortConstants.ListSortDateDescending}`, { + signal: mapfixController.signal, + }); + if (res.ok) { + const data: MapfixList = await res.json(); + setMapfixes(data); + } + } catch (error) { + console.error("Failed to fetch mapfixes:", error); + } finally { + setIsLoadingMapfixes(false); + } + } + + async function fetchSubmissions(): Promise { + setIsLoadingSubmissions(true); + try { + const res = await fetch(`/api/submissions?Page=1&Limit=${itemsPerSection}&Sort=${ListSortConstants.ListSortDateDescending}`, { + signal: submissionsController.signal, + }); + if (res.ok) { + const data: SubmissionList = await res.json(); + setSubmissions(data); + } + } catch (error) { + console.error("Failed to fetch submissions:", error); + } finally { + setIsLoadingSubmissions(false); + } + } + + fetchMapFixes(); + fetchSubmissions(); + + return () => { + mapfixController.abort(); + submissionsController.abort(); + }; + }, []); + + const isLoading: boolean = isLoadingMapfixes || isLoadingSubmissions; + + if (isLoading && (!mapfixes || !submissions)) { + return +
+ + + + Loading content... + + +
+
; + } + + const renderMapfixCard = (mapfix: MapfixInfo): React.ReactNode => ( + + ); + + const renderSubmissionCard = (submission: SubmissionInfo): React.ReactNode => ( + + ); + return ( - + + +
+ + Welcome to the Maps Service! + + + + Contribute to the community + + + Help improve maps by submitting fixes or creating new maps submissions for the community. + + + + + Submit Map + + + + + Create Map Fix + + + + + + {/* Submissions Carousel */} + {submissions && ( + + title="Recent Submissions" + items={submissions.Submissions} + renderItem={renderSubmissionCard} + viewAllLink="/submissions" + /> + )} + + {/* Map Fixes Carousel */} + {mapfixes && ( + + title="Recent Map Fixes" + items={mapfixes.Mapfixes} + renderItem={renderMapfixCard} + viewAllLink="/mapfixes" + /> + )} +
+
+
); } \ No newline at end of file diff --git a/web/src/app/submissions/page.tsx b/web/src/app/submissions/page.tsx index bd67b33..c81e090 100644 --- a/web/src/app/submissions/page.tsx +++ b/web/src/app/submissions/page.tsx @@ -1,112 +1,137 @@ '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, Container} 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} - - -
-
- {currentCards.map((submission) => ( - - ))} -
-
+ +
+ + + Home + + Submissions + + + Submissions + + + Explore all submitted maps from the community. + +
+ {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, diff --git a/web/src/app/ts/Submission.ts b/web/src/app/ts/Submission.ts index 4904726..89d9cd8 100644 --- a/web/src/app/ts/Submission.ts +++ b/web/src/app/ts/Submission.ts @@ -17,7 +17,7 @@ interface SubmissionInfo { readonly DisplayName: string, readonly Creator: string, readonly GameID: number, - readonly Date: number, + readonly CreatedAt: number, readonly Submitter: number, readonly AssetID: number, readonly AssetVersion: number,