From c04bc9a36ebfcdf6d8e2db8a6cdeb44062b66bec Mon Sep 17 00:00:00 2001 From: Wolfboy248 Date: Tue, 19 Aug 2025 13:23:17 +0200 Subject: organised pages, started work on theme --- frontend/src/pages/About/About.tsx | 36 ++ frontend/src/pages/Games/Games.tsx | 29 ++ frontend/src/pages/Home/Homepage.tsx | 31 ++ frontend/src/pages/Maplist/Maplist.tsx | 249 ++++++++++++ frontend/src/pages/Maps/Maps.tsx | 185 +++++++++ frontend/src/pages/Profile/Profile.tsx | 633 +++++++++++++++++++++++++++++++ frontend/src/pages/Rankings/Rankings.tsx | 203 ++++++++++ frontend/src/pages/Rules/Rules.tsx | 37 ++ frontend/src/pages/User/User.tsx | 410 ++++++++++++++++++++ 9 files changed, 1813 insertions(+) create mode 100644 frontend/src/pages/About/About.tsx create mode 100644 frontend/src/pages/Games/Games.tsx create mode 100644 frontend/src/pages/Home/Homepage.tsx create mode 100644 frontend/src/pages/Maplist/Maplist.tsx create mode 100644 frontend/src/pages/Maps/Maps.tsx create mode 100644 frontend/src/pages/Profile/Profile.tsx create mode 100644 frontend/src/pages/Rankings/Rankings.tsx create mode 100644 frontend/src/pages/Rules/Rules.tsx create mode 100644 frontend/src/pages/User/User.tsx (limited to 'frontend') diff --git a/frontend/src/pages/About/About.tsx b/frontend/src/pages/About/About.tsx new file mode 100644 index 0000000..7802d75 --- /dev/null +++ b/frontend/src/pages/About/About.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import ReactMarkdown from "react-markdown"; +import { Helmet } from "react-helmet"; + +const About: React.FC = () => { + const [aboutText, setAboutText] = React.useState(""); + + React.useEffect(() => { + const fetchReadme = async () => { + try { + const response = await fetch( + "https://raw.githubusercontent.com/pektezol/lphub/main/README.md" + ); + if (!response.ok) { + throw new Error("Failed to fetch README"); + } + const readmeText = await response.text(); + setAboutText(readmeText); + } catch (error) { + console.error("Error fetching README:", error); + } + }; + fetchReadme(); + }, []); + + return ( +
+ + LPHUB | About + + {aboutText} +
+ ); +}; + +export default About; diff --git a/frontend/src/pages/Games/Games.tsx b/frontend/src/pages/Games/Games.tsx new file mode 100644 index 0000000..e23b245 --- /dev/null +++ b/frontend/src/pages/Games/Games.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Helmet } from "react-helmet"; + +import GameEntry from "@components/GameEntry.tsx"; +import { Game } from "@customTypes/Game.ts"; + +interface GamesProps { + games: Game[]; +} + +const Games: React.FC = ({ games }) => { + return ( +
+ + LPHUB | Games + +
+

Games

+
+ {games.map((game, index) => ( + + ))} +
+
+
+ ); +}; + +export default Games; diff --git a/frontend/src/pages/Home/Homepage.tsx b/frontend/src/pages/Home/Homepage.tsx new file mode 100644 index 0000000..b4ac3b0 --- /dev/null +++ b/frontend/src/pages/Home/Homepage.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Helmet } from "react-helmet"; + +const Homepage: React.FC = () => { + return ( +
+ + LPHUB | Homepage + +
+

+

Welcome to Least Portals Hub!

+

+ At the moment, LPHUB is in beta state. This means that the site has + only the core functionalities enabled for providing both collaborative + information and competitive leaderboards. +

+

+ The website should feel intuitive to navigate around. For any type of + feedback, reach us at LPHUB Discord server. +

+

+ By using LPHUB, you agree that you have read the 'Leaderboard Rules' + and the 'About LPHUB' pages. +

+
+
+ ); +}; + +export default Homepage; diff --git a/frontend/src/pages/Maplist/Maplist.tsx b/frontend/src/pages/Maplist/Maplist.tsx new file mode 100644 index 0000000..572eb27 --- /dev/null +++ b/frontend/src/pages/Maplist/Maplist.tsx @@ -0,0 +1,249 @@ +import React, { useEffect } from "react"; +import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; +import { Helmet } from "react-helmet"; + +import { API } from "@api/Api.ts"; +import { Game } from "@customTypes/Game.ts"; +import { GameChapter, GamesChapters } from "@customTypes/Chapters.ts"; + +const Maplist: React.FC = () => { + const [game, setGame] = React.useState(null); + const [catNum, setCatNum] = React.useState(0); + const [id, setId] = React.useState(0); + const [load, setLoad] = React.useState(false); + const [currentlySelected, setCurrentlySelected] = React.useState(0); + const [hasClicked, setHasClicked] = React.useState(false); + const [gameChapters, setGameChapters] = React.useState(); + const [curChapter, setCurChapter] = React.useState(); + const [numChapters, setNumChapters] = React.useState(0); + + const [dropdownActive, setDropdownActive] = React.useState("none"); + + const params = useParams<{ id: string; chapter: string }>(); + const location = useLocation(); + const navigate = useNavigate(); + + function _update_currently_selected(catNum2: number) { + setCurrentlySelected(catNum2); + navigate("/games/" + game?.id + "?cat=" + catNum2); + setHasClicked(true); + } + + const _fetch_chapters = async (chapter_id: string) => { + const chapters = await API.get_chapters(chapter_id); + setCurChapter(chapters); + }; + + const _handle_dropdown_click = () => { + if (dropdownActive === "none") { + setDropdownActive("block"); + } else { + setDropdownActive("none"); + } + }; + + // im sorry but im too lazy to fix this right now + useEffect(() => { + // gameID + const gameId = parseFloat(params.id || ""); + setId(gameId); + + // location query params + const queryParams = new URLSearchParams(location.search); + if (queryParams.get("chapter")) { + let cat = parseFloat(queryParams.get("chapter") || ""); + if (gameId === 2) { + cat += 10; + } + _fetch_chapters(cat.toString()); + } + + const _fetch_game = async () => { + const games = await API.get_games(); + const foundGame = games.find(game => game.id === gameId); + // console.log(foundGame) + if (foundGame) { + setGame(foundGame); + setLoad(false); + } + }; + + const _fetch_game_chapters = async () => { + const games_chapters = await API.get_games_chapters(gameId.toString()); + setGameChapters(games_chapters); + setNumChapters(games_chapters.chapters.length); + }; + + setLoad(true); + _fetch_game(); + _fetch_game_chapters(); + }, [location.search]); + + useEffect(() => { + const queryParams = new URLSearchParams(location.search); + if (gameChapters !== undefined && !queryParams.get("chapter")) { + _fetch_chapters(gameChapters!.chapters[0].id.toString()); + } + }, [gameChapters, location.search]); + + return ( +
+ + LPHUB | Maplist + + +
+ + + +
+ + {load ? ( +
+ ) : ( +
+

+ {game?.name} +

+ +
+
+
+

+ { + game?.category_portals.find( + obj => obj.category.id === catNum + 1 + )?.portal_count + } +

+

+ portals +

+
+ +
+ {game?.category_portals.map((cat, index) => ( + + ))} +
+
+
+ +
+
+
+ + {curChapter?.chapter.name.split(" - ")[0]} + +
+
+ + {curChapter?.chapter.name.split(" - ")[1]} + + +
+ \ +
+ {gameChapters?.chapters.map((chapter, i) => { + return ( +
{ + _fetch_chapters(chapter.id.toString()); + _handle_dropdown_click(); + }} + > + {chapter.name} +
+ ); + })} +
+
+ +
+ {curChapter?.maps.map((map, i) => { + return ( +
+ + + {map.name} + +
+
+ + {map.is_disabled + ? map.category_portals[0].portal_count + : map.category_portals.find( + obj => obj.category.id === catNum + 1 + )?.portal_count} + + + portals + +
+
+ +
+
+ {[1, 2, 3, 4, 5].map((point) => ( +
+ ))} +
+
+ +
+ ); + })} +
+
+
+ )} +
+ ); +}; + +export default Maplist; diff --git a/frontend/src/pages/Maps/Maps.tsx b/frontend/src/pages/Maps/Maps.tsx new file mode 100644 index 0000000..75e3635 --- /dev/null +++ b/frontend/src/pages/Maps/Maps.tsx @@ -0,0 +1,185 @@ +import React from "react"; +import { Link, useLocation } from "react-router-dom"; +import { Helmet } from "react-helmet"; + +import { PortalIcon, FlagIcon, ChatIcon } from "../../images/Images.tsx"; +import Summary from "@components/Summary.tsx"; +import Leaderboards from "@components/Leaderboards.tsx"; +import Discussions from "@components/Discussions.tsx"; +import ModMenu from "@components/ModMenu.tsx"; +import { MapDiscussions, MapLeaderboard, MapSummary } from "@customTypes/Map.ts"; +import { API } from "@api/Api.ts"; + +interface MapProps { + token?: string; + isModerator: boolean; +} + +const Maps: React.FC = ({ token, isModerator }) => { + const [selectedRun, setSelectedRun] = React.useState(0); + + const [mapSummaryData, setMapSummaryData] = React.useState< + MapSummary | undefined + >(undefined); + const [mapLeaderboardData, setMapLeaderboardData] = React.useState< + MapLeaderboard | undefined + >(undefined); + const [mapDiscussionsData, setMapDiscussionsData] = React.useState< + MapDiscussions | undefined + >(undefined); + + const [navState, setNavState] = React.useState(0); + + const location = useLocation(); + + const mapID = location.pathname.split("/")[2]; + + const _fetch_map_summary = React.useCallback(async () => { + const mapSummary = await API.get_map_summary(mapID); + setMapSummaryData(mapSummary); + }, [mapID]); + + const _fetch_map_leaderboards = React.useCallback(async () => { + const mapLeaderboards = await API.get_map_leaderboard(mapID, "1"); + setMapLeaderboardData(mapLeaderboards); + }, [mapID]); + + const _fetch_map_discussions = React.useCallback(async () => { + const mapDiscussions = await API.get_map_discussions(mapID); + setMapDiscussionsData(mapDiscussions); + }, [mapID]); + + React.useEffect(() => { + _fetch_map_summary(); + _fetch_map_leaderboards(); + _fetch_map_discussions(); + }, [ + mapID, + _fetch_map_discussions, + _fetch_map_leaderboards, + _fetch_map_summary, + ]); + + if (!mapSummaryData) { + // loading placeholder + return ( + <> +
+
+
+ + + +
+
+ +
+ + + +
+ +
+
+ + ); + } + + return ( + <> + + LPHUB | {mapSummaryData.map.map_name} + + + {isModerator && ( + + )} + +
+ +
+
+
+
+ + + + + + +
+ + {mapSummaryData.map.map_name} + +
+
+ +
+ + + +
+ + {navState === 0 && ( + + )} + {navState === 1 && } + {navState === 2 && ( + _fetch_map_discussions()} + /> + )} +
+ + ); +}; + +export default Maps; diff --git a/frontend/src/pages/Profile/Profile.tsx b/frontend/src/pages/Profile/Profile.tsx new file mode 100644 index 0000000..9aac386 --- /dev/null +++ b/frontend/src/pages/Profile/Profile.tsx @@ -0,0 +1,633 @@ +import React from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { Helmet } from "react-helmet"; + +import { + SteamIcon, + TwitchIcon, + YouTubeIcon, + PortalIcon, + FlagIcon, + StatisticsIcon, + SortIcon, + ThreedotIcon, + DownloadIcon, + HistoryIcon, + DeleteIcon, +} from "@images/Images"; +import { UserProfile } from "@customTypes/Profile.ts"; +import { Game, GameChapters } from "@customTypes/Game.ts"; +import { Map } from "@customTypes/Map.ts"; +import { ticks_to_time } from "@utils/Time.ts"; +import { API } from "@api/Api.ts"; +import useConfirm from "@hooks/UseConfirm.tsx"; +import useMessage from "@hooks/UseMessage.tsx"; +import useMessageLoad from "@hooks/UseMessageLoad.tsx"; + +interface ProfileProps { + profile?: UserProfile; + token?: string; + gameData: Game[]; + onDeleteRecord: () => void; +} + +const Profile: React.FC = ({ + profile, + token, + gameData, + onDeleteRecord, +}) => { + const { confirm, ConfirmDialogComponent } = useConfirm(); + const { message, MessageDialogComponent } = useMessage(); + const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = + useMessageLoad(); + const [navState, setNavState] = React.useState(0); + const [pageNumber, setPageNumber] = React.useState(1); + const [pageMax, setPageMax] = React.useState(0); + + const [game, setGame] = React.useState("0"); + const [chapter, setChapter] = React.useState("0"); + const [chapterData, setChapterData] = React.useState( + null + ); + const [maps, setMaps] = React.useState([]); + + const navigate = useNavigate(); + + const _update_profile = () => { + if (token) { + API.post_profile(token).then(() => navigate(0)); + } + }; + + const _get_game_chapters = React.useCallback(async () => { + if (game && game !== "0") { + const gameChapters = await API.get_games_chapters(game); + setChapterData(gameChapters); + } else if (game && game === "0") { + setPageMax(Math.ceil(profile!.records.length / 20)); + setPageNumber(1); + } + }, [game, profile]); + + const _get_game_maps = React.useCallback(async () => { + if (chapter === "0") { + const gameMaps = await API.get_game_maps(game); + setMaps(gameMaps); + setPageMax(Math.ceil(gameMaps.length / 20)); + setPageNumber(1); + } else { + const gameChapters = await API.get_chapters(chapter); + setMaps(gameChapters.maps); + setPageMax(Math.ceil(gameChapters.maps.length / 20)); + setPageNumber(1); + } + }, [chapter, game]); + + const _delete_submission = async (map_id: number, record_id: number) => { + const userConfirmed = await confirm( + "Delete Record", + "Are you sure you want to delete this record?" + ); + + if (!userConfirmed) { + return; + } + + messageLoad("Deleting..."); + + const api_success = await API.delete_map_record(token!, map_id, record_id); + messageLoadClose(); + if (api_success) { + await message("Delete Record", "Successfully deleted record."); + onDeleteRecord(); + } else { + await message("Delete Record", "Could not delete record."); + } + }; + + React.useEffect(() => { + if (!profile) { + navigate("/"); + } + }, [profile, navigate]); + + React.useEffect(() => { + if (profile) { + _get_game_chapters(); + } + }, [profile, game, _get_game_chapters]); + + React.useEffect(() => { + if (profile && game !== "0") { + _get_game_maps(); + } + }, [profile, game, chapter, chapterData, _get_game_maps]); + + if (!profile) { + return <>; + } + + return ( +
+ + LPHUB | {profile.user_name} + + + {MessageDialogComponent} + {MessageDialogLoadComponent} + {ConfirmDialogComponent} + +
+
+ {profile.profile ? ( +
+ profile-image + Refresh +
+ ) : ( +
+ profile-image +
+ )} + +
+
+
{profile.user_name}
+
+ {profile.country_code === "XX" ? ( + "" + ) : ( + {profile.country_code} + )} +
+
+ {profile.titles.map(e => ( + + {e.name} + + ))} +
+
+
+ {profile.links.steam === "-" ? ( + "" + ) : ( + + Steam + + )} + {profile.links.twitch === "-" ? ( + "" + ) : ( + + Twitch + + )} + {profile.links.youtube === "-" ? ( + "" + ) : ( + + Youtube + + )} + {profile.links.p2sr === "-" ? ( + "" + ) : ( + + P2SR + + )} +
+
+
+
+ Overall + + {profile.rankings.overall.rank === 0 + ? "N/A " + : "#" + profile.rankings.overall.rank + " "} + + ({profile.rankings.overall.completion_count}/ + {profile.rankings.overall.completion_total}) + + +
+
+ Singleplayer + + {profile.rankings.singleplayer.rank === 0 + ? "N/A " + : "#" + profile.rankings.singleplayer.rank + " "} + + ({profile.rankings.singleplayer.completion_count}/ + {profile.rankings.singleplayer.completion_total}) + + +
+
+ Cooperative + + {profile.rankings.cooperative.rank === 0 + ? "N/A " + : "#" + profile.rankings.cooperative.rank + " "} + + ({profile.rankings.cooperative.completion_count}/ + {profile.rankings.cooperative.completion_total}) + + +
+
+
+ +
+ + +
+ +
+
+ {gameData === null ? ( + + ) : ( + + )} + + {game === "0" ? ( + + ) : chapterData === null ? ( + + ) : ( + + )} +
+
+ + Map Name + + + + Portals + + + + WRΔ + + + + Time + + + + + Rank + + + + Date + + +
+
+ + + {pageNumber}/{pageMax} + + +
+
+
+
+
+ {game === "0" ? ( + profile.records + .sort((a, b) => a.map_id - b.map_id) + .map((r, index) => + Math.ceil((index + 1) / 20) === pageNumber ? ( + + + + {i === 0 && r.scores.length > 1 ? ( + + ) : ( + "" + )} + + + ))} + + ) : ( + "" + ) + ) + ) : maps ? ( + maps + .filter(e => e.is_disabled === false) + .sort((a, b) => a.id - b.id) + .map((r, index) => { + if (Math.ceil((index + 1) / 20) === pageNumber) { + let record = profile.records.find(e => e.map_id === r.id); + return record === undefined ? ( + + ) : ( + + + + {i === 0 && record!.scores.length > 1 ? ( + + ) : ( + "" + )} + + + ))} + + ); + } else { + return null; + } + }) + ) : ( + <>{console.warn(maps)} + )} +
+
+
+
+ ); +}; + +export default Profile; diff --git a/frontend/src/pages/Rankings/Rankings.tsx b/frontend/src/pages/Rankings/Rankings.tsx new file mode 100644 index 0000000..57b875f --- /dev/null +++ b/frontend/src/pages/Rankings/Rankings.tsx @@ -0,0 +1,203 @@ +import React, { useEffect } from "react"; +import { Helmet } from "react-helmet"; + +import RankingEntry from "@components/RankingEntry.tsx"; +import { + Ranking, + SteamRanking, + RankingType, + SteamRankingType, +} from "@customTypes/Ranking.ts"; +import { API } from "@api/Api.ts"; + +import "@css/Rankings.css"; + +enum LeaderboardTypes { + official, + unofficial, +} + +enum RankingCategories { + rankings_overall, + rankings_multiplayer, + rankings_singleplayer, +} + +const Rankings: React.FC = () => { + const [leaderboardData, setLeaderboardData] = React.useState< + Ranking | SteamRanking + >(); + const [currentLeaderboard, setCurrentLeaderboard] = React.useState< + RankingType[] | SteamRankingType[] + >(); + const [currentRankingType, setCurrentRankingType] = + React.useState(LeaderboardTypes.official); + + const [leaderboardLoad, setLeaderboardLoad] = React.useState(false); + + const [currentLeaderboardType, setCurrentLeaderboardType] = + React.useState(RankingCategories.rankings_singleplayer); + const [load, setLoad] = React.useState(false); + + const _fetch_rankings = React.useCallback(async () => { + setLeaderboardLoad(false); + const rankings = await API.get_official_rankings(); + setLeaderboardData(rankings); + if (currentLeaderboardType === RankingCategories.rankings_singleplayer) { + setCurrentLeaderboard(rankings.rankings_singleplayer); + } else if ( + currentLeaderboardType === RankingCategories.rankings_multiplayer + ) { + setCurrentLeaderboard(rankings.rankings_multiplayer); + } else { + setCurrentLeaderboard(rankings.rankings_overall); + } + setLoad(true); + setLeaderboardLoad(true); + }, [currentLeaderboardType]); + + const __dev_fetch_unofficial_rankings = async () => { + try { + setLeaderboardLoad(false); + const rankings = await API.get_unofficial_rankings(); + setLeaderboardData(rankings); + if (currentLeaderboardType === RankingCategories.rankings_singleplayer) { + // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) + setCurrentLeaderboard(rankings.rankings_singleplayer); + } else if ( + currentLeaderboardType === RankingCategories.rankings_multiplayer + ) { + setCurrentLeaderboard(rankings.rankings_multiplayer); + } else { + setCurrentLeaderboard(rankings.rankings_overall); + } + setLeaderboardLoad(true); + } catch (e) { + console.log(e); + } + }; + + const _set_current_leaderboard = React.useCallback( + (ranking_cat: RankingCategories) => { + if (ranking_cat === RankingCategories.rankings_singleplayer) { + setCurrentLeaderboard(leaderboardData!.rankings_singleplayer); + } else if (ranking_cat === RankingCategories.rankings_multiplayer) { + setCurrentLeaderboard(leaderboardData!.rankings_multiplayer); + } else { + setCurrentLeaderboard(leaderboardData!.rankings_overall); + } + + setCurrentLeaderboardType(ranking_cat); + }, + [leaderboardData] + ); + + // unused func + // const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => { + // if (leaderboard_type === LeaderboardTypes.official) { + // _fetch_rankings(); + // } else { + // } + // }; + + useEffect(() => { + _fetch_rankings(); + }, [_fetch_rankings]); + + return ( +
+ + LPHUB | Rankings + +
+
+ + +
+
+
+
+ + + +
+
+ + {load ? ( +
+
+
+ Rank + Player + Portals +
+ +
+ + {leaderboardLoad && + currentLeaderboard?.map((curRankingData, i) => { + return ( + + ); + })} + + {leaderboardLoad ? null : ( +
+ +
+ )} +
+
+ ) : null} +
+ ); +}; + +export default Rankings; diff --git a/frontend/src/pages/Rules/Rules.tsx b/frontend/src/pages/Rules/Rules.tsx new file mode 100644 index 0000000..9c7885c --- /dev/null +++ b/frontend/src/pages/Rules/Rules.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import ReactMarkdown from "react-markdown"; +import { Helmet } from "react-helmet"; + +const Rules: React.FC = () => { + const [rulesText, setRulesText] = React.useState(""); + + React.useEffect(() => { + const fetchRules = async () => { + try { + const response = await fetch( + "https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md" + ); + if (!response.ok) { + throw new Error("Failed to fetch README"); + } + const rulesText = await response.text(); + setRulesText(rulesText); + } catch (error) { + console.error("Error fetching Rules:", error); + } + // setRulesText(rulesText) + }; + fetchRules(); + }, []); + + return ( +
+ + LPHUB | Rules + + {rulesText} +
+ ); +}; + +export default Rules; diff --git a/frontend/src/pages/User/User.tsx b/frontend/src/pages/User/User.tsx new file mode 100644 index 0000000..30c9e45 --- /dev/null +++ b/frontend/src/pages/User/User.tsx @@ -0,0 +1,410 @@ +import React from "react"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import { Helmet } from "react-helmet"; + +import { + SteamIcon, + TwitchIcon, + YouTubeIcon, + PortalIcon, + FlagIcon, + StatisticsIcon, + SortIcon, + ThreedotIcon, + DownloadIcon, + HistoryIcon, +} from "@images/Images"; +import { UserProfile } from "@customTypes/Profile.ts"; +import { Game, GameChapters } from "@customTypes/Game.ts"; +import { Map } from "@customTypes/Map.ts"; +import { API } from "@api/Api.ts"; +import { ticks_to_time } from "@utils/Time.ts"; +import useMessage from "@hooks/UseMessage.tsx"; + +interface UserProps { + profile?: UserProfile; + token?: string; + gameData: Game[]; +} + +const User: React.FC = ({ token, profile, gameData }) => { + const { message, MessageDialogComponent } = useMessage(); + + const [user, setUser] = React.useState(undefined); + + const [navState, setNavState] = React.useState(0); + const [pageNumber, setPageNumber] = React.useState(1); + const [pageMax, setPageMax] = React.useState(0); + + const [game, setGame] = React.useState("0"); + const [chapter, setChapter] = React.useState("0"); + const [chapterData, setChapterData] = React.useState( + null + ); + const [maps, setMaps] = React.useState([]); + + const location = useLocation(); + const navigate = useNavigate(); + + const _fetch_user = React.useCallback(async () => { + const userID = location.pathname.split("/")[2]; + if (token && profile && profile.profile && profile.steam_id === userID) { + navigate("/profile"); + return; + } + const userData = await API.get_user(userID); + setUser(userData); + }, [location.pathname, token, profile, navigate]); + + const _get_game_chapters = React.useCallback(async () => { + if (game !== "0") { + const gameChapters = await API.get_games_chapters(game); + setChapterData(gameChapters); + } else { + setPageMax(Math.ceil(user!.records.length / 20)); + setPageNumber(1); + } + }, [game, user]); + + const _get_game_maps = React.useCallback(async () => { + if (chapter === "0") { + const gameMaps = await API.get_game_maps(game); + setMaps(gameMaps); + setPageMax(Math.ceil(gameMaps.length / 20)); + setPageNumber(1); + } else { + const gameChapters = await API.get_chapters(chapter); + setMaps(gameChapters.maps); + setPageMax(Math.ceil(gameChapters.maps.length / 20)); + setPageNumber(1); + } + }, [chapter, game]); + + React.useEffect(() => { + _fetch_user(); + }, [location, _fetch_user]); + + React.useEffect(() => { + if (user) { + _get_game_chapters(); + } + }, [user, game, location, _get_game_chapters]); + + React.useEffect(() => { + if (user && game !== "0") { + _get_game_maps(); + } + }, [user, game, chapter, location, _get_game_maps]); + + if (!user) { + return ( +
+ Loading... +
+ ); + } + + return ( +
+ + LPHUB | {user.user_name} + + + + {MessageDialogComponent} + +
+
+ Profile +
+

+ {user.user_name} +

+ {user.country_code !== "XX" && ( +
+ {user.country_code} + {user.country_code} +
+ )} +
+ {user.titles.map((title, index) => ( + + {title.name} + + ))} +
+
+
+ {user.links.steam !== "-" && ( + + Steam + + )} + {user.links.twitch !== "-" && ( + + Twitch + + )} + {user.links.youtube !== "-" && ( + + YouTube + + )} + {user.links.p2sr !== "-" && ( + + P2SR + + )} +
+
+ +
+
+
Overall
+
+ {user.rankings.overall.rank === 0 ? "N/A" : `#${user.rankings.overall.rank}`} +
+
+ {user.rankings.overall.completion_count}/{user.rankings.overall.completion_total} +
+
+
+
Singleplayer
+
+ {user.rankings.singleplayer.rank === 0 ? "N/A" : `#${user.rankings.singleplayer.rank}`} +
+
+ {user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total} +
+
+
+
Cooperative
+
+ {user.rankings.cooperative.rank === 0 ? "N/A" : `#${user.rankings.cooperative.rank}`} +
+
+ {user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total} +
+
+
+
+ +
+ + +
+ + {navState === 0 && ( +
+
+ + + +
+ +
+
+ Map Name + Sort +
+
+ Portals + Sort +
+
+ WRΔ + Sort +
+
+ Time + Sort +
+
+
+ Rank + Sort +
+
+ Date + Sort +
+
+ + {pageNumber}/{pageMax} + +
+
+ +
+ {game === "0" ? ( + user.records + .sort((a, b) => a.map_id - b.map_id) + .map((record, index) => + Math.ceil((index + 1) / 20) === pageNumber ? ( +
+ + {record.map_name} + + {record.scores[0]?.score_count || 'N/A'} + 0 ? 'text-[#dc3545]' : ''}`}> + {record.scores[0]?.score_count - record.map_wr_count > 0 + ? `+${record.scores[0].score_count - record.map_wr_count}` + : '–'} + + {record.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'} + + #{record.placement} + {record.scores[0]?.date.split("T")[0] || 'N/A'} +
+ + + {record.scores.length > 1 && ( + + )} +
+
+ ) : null + ) + ) : ( + maps + ?.filter(map => !map.is_disabled) + .sort((a, b) => a.id - b.id) + .map((map, index) => { + if (Math.ceil((index + 1) / 20) !== pageNumber) return null; + + const record = user.records.find(r => r.map_id === map.id); + + return ( +
+ + {map.name} + + {record?.scores[0]?.score_count || 'N/A'} + 0 ? 'text-[#dc3545]' : ''}`}> + {record?.scores[0]?.score_count && record.scores[0].score_count - record.map_wr_count > 0 + ? `+${record.scores[0].score_count - record.map_wr_count}` + : '–'} + + {record?.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'} + + {record ? `#${record.placement}` : 'N/A'} + {record?.scores[0]?.date.split("T")[0] || 'N/A'} +
+ {record?.scores[0] && ( + <> + + + {record.scores.length > 1 && ( + + )} + + )} +
+
+ ); + }) + )} +
+
+ )} +
+ ); +}; + +export default User; -- cgit v1.2.3