From 08bf4412c847f5cf41be8bec8c08e7d985fbe759 Mon Sep 17 00:00:00 2001 From: itzaname Date: Sat, 7 Jun 2025 19:34:02 -0400 Subject: [PATCH] Rework map list page --- web/src/app/maps/page.tsx | 330 ++++++++++++++++++++++++++++++++------ 1 file changed, 284 insertions(+), 46 deletions(-) diff --git a/web/src/app/maps/page.tsx b/web/src/app/maps/page.tsx index 7e62df3..4cc9ce7 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 3: + 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 3: // 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