diff options
| author | Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> | 2025-10-22 13:59:12 +0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-22 12:59:12 +0300 |
| commit | 69aeb7889ac136a8e4fbe7de1330298e30345479 (patch) | |
| tree | 6b2cd2d420105dc7ffad3c3649df359f634cae77 /frontend/src | |
| parent | feat/rankings: update wr for 3 maps (#279) (diff) | |
| download | lphub-69aeb7889ac136a8e4fbe7de1330298e30345479.tar.gz lphub-69aeb7889ac136a8e4fbe7de1330298e30345479.tar.bz2 lphub-69aeb7889ac136a8e4fbe7de1330298e30345479.zip | |
feat/frontend: switch to vite, update node to v22 (#281)
Diffstat (limited to 'frontend/src')
41 files changed, 789 insertions, 795 deletions
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bdd3adc..e7f225c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx | |||
| @@ -1,24 +1,24 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Routes, Route } from "react-router-dom"; | 2 | import { Routes, Route } from "react-router-dom"; |
| 3 | import { Helmet } from "react-helmet"; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import { UserProfile } from '@customTypes/Profile'; | 5 | import { UserProfile } from "@customTypes/Profile"; |
| 6 | import Sidebar from './components/Sidebar'; | 6 | import Sidebar from "./components/Sidebar"; |
| 7 | import "./App.css"; | 7 | import "./App.css"; |
| 8 | 8 | ||
| 9 | import Profile from '@pages/Profile'; | 9 | import Profile from "@pages/Profile"; |
| 10 | import Games from '@pages/Games'; | 10 | import Games from "@pages/Games"; |
| 11 | import Maps from '@pages/Maps'; | 11 | import Maps from "@pages/Maps"; |
| 12 | import User from '@pages/User'; | 12 | import User from "@pages/User"; |
| 13 | import Homepage from '@pages/Homepage'; | 13 | import Homepage from "@pages/Homepage"; |
| 14 | import UploadRunDialog from './components/UploadRunDialog'; | 14 | import UploadRunDialog from "./components/UploadRunDialog"; |
| 15 | import Rules from '@pages/Rules'; | 15 | import Rules from "@pages/Rules"; |
| 16 | import About from '@pages/About'; | 16 | import About from "@pages/About"; |
| 17 | import { Game } from '@customTypes/Game'; | 17 | import { Game } from "@customTypes/Game"; |
| 18 | import { API } from './api/Api'; | 18 | import { API } from "./api/Api"; |
| 19 | import Maplist from '@pages/Maplist'; | 19 | import Maplist from "@pages/Maplist"; |
| 20 | import Rankings from '@pages/Rankings'; | 20 | import Rankings from "@pages/Rankings"; |
| 21 | import { get_user_id_from_token, get_user_mod_from_token } from './utils/Jwt'; | 21 | import { get_user_id_from_token, get_user_mod_from_token } from "./utils/Jwt"; |
| 22 | 22 | ||
| 23 | const App: React.FC = () => { | 23 | const App: React.FC = () => { |
| 24 | const [token, setToken] = React.useState<string | undefined>(undefined); | 24 | const [token, setToken] = React.useState<string | undefined>(undefined); |
diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts index c84714d..df787e3 100644 --- a/frontend/src/api/Api.ts +++ b/frontend/src/api/Api.ts | |||
| @@ -1,11 +1,11 @@ | |||
| 1 | import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '@customTypes/Content'; | 1 | import { MapDiscussionContent, ModMenuContent } from "@customTypes/Content"; |
| 2 | import { delete_token, get_token } from '@api/Auth'; | 2 | import { delete_token, get_token } from "@api/Auth"; |
| 3 | import { get_user, get_profile, post_profile } from '@api/User'; | 3 | import { get_user, get_profile, post_profile } from "@api/User"; |
| 4 | import { get_games, get_chapters, get_games_chapters, get_game_maps, get_search } from '@api/Games'; | 4 | import { get_games, get_chapters, get_games_chapters, get_game_maps, get_search } from "@api/Games"; |
| 5 | import { get_official_rankings, get_unofficial_rankings } from '@api/Rankings'; | 5 | import { get_official_rankings, get_unofficial_rankings } from "@api/Rankings"; |
| 6 | import { get_map_summary, get_map_leaderboard, get_map_discussions, get_map_discussion, post_map_discussion, post_map_discussion_comment, delete_map_discussion, post_record, delete_map_record } from '@api/Maps'; | 6 | import { get_map_summary, get_map_leaderboard, get_map_discussions, get_map_discussion, post_map_discussion, post_map_discussion_comment, delete_map_discussion, post_record, delete_map_record } from "@api/Maps"; |
| 7 | import { delete_map_summary, post_map_summary, put_map_image, put_map_summary } from '@api/Mod'; | 7 | import { delete_map_summary, post_map_summary, put_map_image, put_map_summary } from "@api/Mod"; |
| 8 | import { UploadRunContent } from '@customTypes/Content'; | 8 | import { UploadRunContent } from "@customTypes/Content"; |
| 9 | 9 | ||
| 10 | // add new api call function entries here | 10 | // add new api call function entries here |
| 11 | // example usage: API.get_games(); | 11 | // example usage: API.get_games(); |
| @@ -49,7 +49,7 @@ export const API = { | |||
| 49 | delete_map_summary: (token: string, map_id: string, route_id: number) => delete_map_summary(token, map_id, route_id), | 49 | delete_map_summary: (token: string, map_id: string, route_id: number) => delete_map_summary(token, map_id, route_id), |
| 50 | }; | 50 | }; |
| 51 | 51 | ||
| 52 | const BASE_API_URL: string = "/api/v1/" | 52 | const BASE_API_URL: string = "https://lp.portal2.sr/api/v1/" |
| 53 | 53 | ||
| 54 | export function url(path: string): string { | 54 | export function url(path: string): string { |
| 55 | return BASE_API_URL + path; | 55 | return BASE_API_URL + path; |
diff --git a/frontend/src/api/Auth.ts b/frontend/src/api/Auth.ts index 875c7e5..56e0f6a 100644 --- a/frontend/src/api/Auth.ts +++ b/frontend/src/api/Auth.ts | |||
| @@ -2,7 +2,7 @@ import axios from "axios"; | |||
| 2 | import { url } from "@api/Api"; | 2 | import { url } from "@api/Api"; |
| 3 | 3 | ||
| 4 | export const get_token = async (): Promise<string | undefined> => { | 4 | export const get_token = async (): Promise<string | undefined> => { |
| 5 | const response = await axios.get(url(`token`)) | 5 | const response = await axios.get(url("token")) |
| 6 | if (!response.data.success) { | 6 | if (!response.data.success) { |
| 7 | return undefined; | 7 | return undefined; |
| 8 | } | 8 | } |
diff --git a/frontend/src/api/Games.ts b/frontend/src/api/Games.ts index 72bb4b3..98c65e7 100644 --- a/frontend/src/api/Games.ts +++ b/frontend/src/api/Games.ts | |||
| @@ -6,7 +6,7 @@ import { Map } from "@customTypes/Map"; | |||
| 6 | import { Search } from "@customTypes/Search"; | 6 | import { Search } from "@customTypes/Search"; |
| 7 | 7 | ||
| 8 | export const get_games = async (): Promise<Game[]> => { | 8 | export const get_games = async (): Promise<Game[]> => { |
| 9 | const response = await axios.get(url(`games`)) | 9 | const response = await axios.get(url("games")) |
| 10 | return response.data.data; | 10 | return response.data.data; |
| 11 | }; | 11 | }; |
| 12 | 12 | ||
diff --git a/frontend/src/api/Maps.ts b/frontend/src/api/Maps.ts index aa967ce..8d29f84 100644 --- a/frontend/src/api/Maps.ts +++ b/frontend/src/api/Maps.ts | |||
| @@ -17,9 +17,9 @@ export const get_map_leaderboard = async (map_id: string, page: string): Promise | |||
| 17 | // map the kind of leaderboard | 17 | // map the kind of leaderboard |
| 18 | data.records = data.records.map((record: any) => { | 18 | data.records = data.records.map((record: any) => { |
| 19 | if (record.host && record.partner) { | 19 | if (record.host && record.partner) { |
| 20 | return { ...record, kind: 'multiplayer' }; | 20 | return { ...record, kind: "multiplayer" }; |
| 21 | } else { | 21 | } else { |
| 22 | return { ...record, kind: 'singleplayer' }; | 22 | return { ...record, kind: "singleplayer" }; |
| 23 | } | 23 | } |
| 24 | }); | 24 | }); |
| 25 | return data; | 25 | return data; |
diff --git a/frontend/src/api/Rankings.ts b/frontend/src/api/Rankings.ts index b8d9bec..a5f4c88 100644 --- a/frontend/src/api/Rankings.ts +++ b/frontend/src/api/Rankings.ts | |||
| @@ -3,11 +3,11 @@ import { url } from "@api/Api"; | |||
| 3 | import { Ranking, SteamRanking } from "@customTypes/Ranking"; | 3 | import { Ranking, SteamRanking } from "@customTypes/Ranking"; |
| 4 | 4 | ||
| 5 | export const get_official_rankings = async (): Promise<Ranking> => { | 5 | export const get_official_rankings = async (): Promise<Ranking> => { |
| 6 | const response = await axios.get(url(`rankings/lphub`)); | 6 | const response = await axios.get(url("rankings/lphub")); |
| 7 | return response.data.data; | 7 | return response.data.data; |
| 8 | }; | 8 | }; |
| 9 | 9 | ||
| 10 | export const get_unofficial_rankings = async (): Promise<SteamRanking> => { | 10 | export const get_unofficial_rankings = async (): Promise<SteamRanking> => { |
| 11 | const response = await axios.get(url(`rankings/steam`)); | 11 | const response = await axios.get(url("rankings/steam")); |
| 12 | return response.data.data; | 12 | return response.data.data; |
| 13 | }; | 13 | }; |
diff --git a/frontend/src/api/User.ts b/frontend/src/api/User.ts index 88da0f2..995902c 100644 --- a/frontend/src/api/User.ts +++ b/frontend/src/api/User.ts | |||
| @@ -8,7 +8,7 @@ export const get_user = async (user_id: string): Promise<UserProfile> => { | |||
| 8 | }; | 8 | }; |
| 9 | 9 | ||
| 10 | export const get_profile = async (token: string): Promise<UserProfile> => { | 10 | export const get_profile = async (token: string): Promise<UserProfile> => { |
| 11 | const response = await axios.get(url(`profile`), { | 11 | const response = await axios.get(url("profile"), { |
| 12 | headers: { | 12 | headers: { |
| 13 | "Authorization": token, | 13 | "Authorization": token, |
| 14 | } | 14 | } |
| @@ -17,7 +17,7 @@ export const get_profile = async (token: string): Promise<UserProfile> => { | |||
| 17 | }; | 17 | }; |
| 18 | 18 | ||
| 19 | export const post_profile = async (token: string) => { | 19 | export const post_profile = async (token: string) => { |
| 20 | const _ = await axios.post(url(`profile`), {}, { | 20 | await axios.post(url("profile"), {}, { |
| 21 | headers: { | 21 | headers: { |
| 22 | "Authorization": token, | 22 | "Authorization": token, |
| 23 | } | 23 | } |
diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx index 44a653b..d8784d2 100644 --- a/frontend/src/components/ConfirmDialog.tsx +++ b/frontend/src/components/ConfirmDialog.tsx | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | 2 | ||
| 3 | import "@css/Dialog.css" | 3 | import "@css/Dialog.css" |
| 4 | 4 | ||
| @@ -10,22 +10,22 @@ interface ConfirmDialogProps { | |||
| 10 | }; | 10 | }; |
| 11 | 11 | ||
| 12 | const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ title, subtitle, onConfirm, onCancel }) => { | 12 | const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ title, subtitle, onConfirm, onCancel }) => { |
| 13 | return ( | 13 | return ( |
| 14 | <div className='dimmer'> | 14 | <div className='dimmer'> |
| 15 | <div className='dialog'> | 15 | <div className='dialog'> |
| 16 | <div className='dialog-element dialog-header'> | 16 | <div className='dialog-element dialog-header'> |
| 17 | <span>{title}</span> | 17 | <span>{title}</span> |
| 18 | </div> | ||
| 19 | <div className='dialog-element dialog-description'> | ||
| 20 | <span>{subtitle}</span> | ||
| 21 | </div> | ||
| 22 | <div className='dialog-element dialog-btns-container'> | ||
| 23 | <button onClick={onCancel}>Cancel</button> | ||
| 24 | <button onClick={onConfirm}>Confirm</button> | ||
| 25 | </div> | ||
| 26 | </div> | ||
| 27 | </div> | 18 | </div> |
| 28 | ) | 19 | <div className='dialog-element dialog-description'> |
| 20 | <span>{subtitle}</span> | ||
| 21 | </div> | ||
| 22 | <div className='dialog-element dialog-btns-container'> | ||
| 23 | <button onClick={onCancel}>Cancel</button> | ||
| 24 | <button onClick={onConfirm}>Confirm</button> | ||
| 25 | </div> | ||
| 26 | </div> | ||
| 27 | </div> | ||
| 28 | ) | ||
| 29 | }; | 29 | }; |
| 30 | 30 | ||
| 31 | export default ConfirmDialog; | 31 | export default ConfirmDialog; |
diff --git a/frontend/src/components/Discussions.tsx b/frontend/src/components/Discussions.tsx index 17ae586..0c37355 100644 --- a/frontend/src/components/Discussions.tsx +++ b/frontend/src/components/Discussions.tsx | |||
| @@ -1,12 +1,12 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | 2 | ||
| 3 | import { MapDiscussion, MapDiscussions, MapDiscussionsDetail } from '@customTypes/Map'; | 3 | import { MapDiscussion, MapDiscussions, MapDiscussionsDetail } from "@customTypes/Map"; |
| 4 | import { MapDiscussionCommentContent, MapDiscussionContent } from '@customTypes/Content'; | 4 | import { MapDiscussionContent } from "@customTypes/Content"; |
| 5 | import { time_ago } from '@utils/Time'; | 5 | import { time_ago } from "@utils/Time"; |
| 6 | import { API } from '@api/Api'; | 6 | import { API } from "@api/Api"; |
| 7 | import "@css/Maps.css" | 7 | import "@css/Maps.css" |
| 8 | import { Link } from 'react-router-dom'; | 8 | import { Link } from "react-router-dom"; |
| 9 | import useConfirm from '@hooks/UseConfirm'; | 9 | import useConfirm from "@hooks/UseConfirm"; |
| 10 | 10 | ||
| 11 | interface DiscussionsProps { | 11 | interface DiscussionsProps { |
| 12 | token?: string | 12 | token?: string |
| @@ -141,7 +141,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ token, data, isModerator, map | |||
| 141 | data ? | 141 | data ? |
| 142 | (<> | 142 | (<> |
| 143 | {data.discussions.filter(f => f.title.includes(discussionSearch)).sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) | 143 | {data.discussions.filter(f => f.title.includes(discussionSearch)).sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) |
| 144 | .map((e, i) => ( | 144 | .map((e) => ( |
| 145 | <div id='discussion-post'> | 145 | <div id='discussion-post'> |
| 146 | <button key={e.id} onClick={() => _open_map_discussion(e.id)}> | 146 | <button key={e.id} onClick={() => _open_map_discussion(e.id)}> |
| 147 | <span>{e.title}</span> | 147 | <span>{e.title}</span> |
diff --git a/frontend/src/components/GameCategory.tsx b/frontend/src/components/GameCategory.tsx index d8879ef..0d76d3e 100644 --- a/frontend/src/components/GameCategory.tsx +++ b/frontend/src/components/GameCategory.tsx | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link } from "react-router-dom"; | 2 | import { Link } from "react-router-dom"; |
| 3 | 3 | ||
| 4 | import { Game, GameCategoryPortals } from '@customTypes/Game'; | 4 | import { Game, GameCategoryPortals } from "@customTypes/Game"; |
| 5 | import "@css/Games.css" | 5 | import "@css/Games.css" |
| 6 | 6 | ||
| 7 | interface GameCategoryProps { | 7 | interface GameCategoryProps { |
| @@ -10,15 +10,15 @@ interface GameCategoryProps { | |||
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | const GameCategory: React.FC<GameCategoryProps> = ({cat, game}) => { | 12 | const GameCategory: React.FC<GameCategoryProps> = ({cat, game}) => { |
| 13 | return ( | 13 | return ( |
| 14 | <Link className="games-page-item-body-item" to={"/games/" + game.id + "?cat=" + cat.category.id}> | 14 | <Link className="games-page-item-body-item" to={"/games/" + game.id + "?cat=" + cat.category.id}> |
| 15 | <div> | 15 | <div> |
| 16 | <span className='games-page-item-body-item-title'>{cat.category.name}</span> | 16 | <span className='games-page-item-body-item-title'>{cat.category.name}</span> |
| 17 | <br /> | 17 | <br /> |
| 18 | <span className='games-page-item-body-item-num'>{cat.portal_count}</span> | 18 | <span className='games-page-item-body-item-num'>{cat.portal_count}</span> |
| 19 | </div> | 19 | </div> |
| 20 | </Link> | 20 | </Link> |
| 21 | ) | 21 | ) |
| 22 | } | 22 | } |
| 23 | 23 | ||
| 24 | export default GameCategory; | 24 | export default GameCategory; |
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx index 3bd2842..454efb1 100644 --- a/frontend/src/components/GameEntry.tsx +++ b/frontend/src/components/GameEntry.tsx | |||
| @@ -1,10 +1,10 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link } from "react-router-dom"; | 2 | import { Link } from "react-router-dom"; |
| 3 | 3 | ||
| 4 | import { Game, GameCategoryPortals } from '@customTypes/Game'; | 4 | import { Game, GameCategoryPortals } from "@customTypes/Game"; |
| 5 | import "@css/Games.css" | 5 | import "@css/Games.css" |
| 6 | 6 | ||
| 7 | import GameCategory from '@components/GameCategory'; | 7 | import GameCategory from "@components/GameCategory"; |
| 8 | 8 | ||
| 9 | interface GameEntryProps { | 9 | interface GameEntryProps { |
| 10 | game: Game; | 10 | game: Game; |
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx index fb614fa..5d4045a 100644 --- a/frontend/src/components/Leaderboards.tsx +++ b/frontend/src/components/Leaderboards.tsx | |||
| @@ -1,9 +1,9 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link, useNavigate } from 'react-router-dom'; | 2 | import { Link, useNavigate } from "react-router-dom"; |
| 3 | 3 | ||
| 4 | import { DownloadIcon, ThreedotIcon } from '@images/Images'; | 4 | import { DownloadIcon, ThreedotIcon } from "@images/Images"; |
| 5 | import { MapLeaderboard } from '@customTypes/Map'; | 5 | import { MapLeaderboard } from "@customTypes/Map"; |
| 6 | import { ticks_to_time, time_ago } from '@utils/Time'; | 6 | import { ticks_to_time, time_ago } from "@utils/Time"; |
| 7 | import { API } from "@api/Api"; | 7 | import { API } from "@api/Api"; |
| 8 | import useMessage from "@hooks/UseMessage"; | 8 | import useMessage from "@hooks/UseMessage"; |
| 9 | import "@css/Maps.css" | 9 | import "@css/Maps.css" |
| @@ -46,80 +46,80 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 46 | }; | 46 | }; |
| 47 | 47 | ||
| 48 | return ( | 48 | return ( |
| 49 | <div> | 49 | <div> |
| 50 | {MessageDialogComponent} | 50 | {MessageDialogComponent} |
| 51 | <section id='section6' className='summary2'> | 51 | <section id='section6' className='summary2'> |
| 52 | |||
| 53 | <div id='leaderboard-top' | ||
| 54 | style={data.map.is_coop ? { gridTemplateColumns: "7.5% 40% 7.5% 15% 15% 15%" } : { gridTemplateColumns: "7.5% 30% 10% 20% 17.5% 15%" }} | ||
| 55 | > | ||
| 56 | <span>Place</span> | ||
| 57 | 52 | ||
| 58 | {data.map.is_coop ? ( | 53 | <div id='leaderboard-top' |
| 59 | <div id='runner'> | 54 | style={data.map.is_coop ? { gridTemplateColumns: "7.5% 40% 7.5% 15% 15% 15%" } : { gridTemplateColumns: "7.5% 30% 10% 20% 17.5% 15%" }} |
| 60 | <span>Blue</span> | 55 | > |
| 61 | <span>Orange</span> | 56 | <span>Place</span> |
| 62 | </div> | 57 | |
| 63 | ) : ( | 58 | {data.map.is_coop ? ( |
| 64 | <span>Runner</span> | 59 | <div id='runner'> |
| 65 | )} | 60 | <span>Blue</span> |
| 66 | 61 | <span>Orange</span> | |
| 67 | <span>Portals</span> | 62 | </div> |
| 68 | <span>Time</span> | 63 | ) : ( |
| 69 | <span>Date</span> | 64 | <span>Runner</span> |
| 70 | <div id='page-number'> | 65 | )} |
| 71 | <div> | 66 | |
| 72 | 67 | <span>Portals</span> | |
| 73 | <button onClick={() => pageNumber === 1 ? null : setPageNumber(prevPageNumber => prevPageNumber - 1)} | 68 | <span>Time</span> |
| 74 | ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> | 69 | <span>Date</span> |
| 75 | <span>{data.pagination.current_page}/{data.pagination.total_pages}</span> | 70 | <div id='page-number'> |
| 76 | <button onClick={() => pageNumber === data.pagination.total_pages ? null : setPageNumber(prevPageNumber => prevPageNumber + 1)} | 71 | <div> |
| 77 | ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> | 72 | |
| 73 | <button onClick={() => pageNumber === 1 ? null : setPageNumber(prevPageNumber => prevPageNumber - 1)} | ||
| 74 | ><i className='triangle' style={{ position: "relative", left: "-5px", }}></i> </button> | ||
| 75 | <span>{data.pagination.current_page}/{data.pagination.total_pages}</span> | ||
| 76 | <button onClick={() => pageNumber === data.pagination.total_pages ? null : setPageNumber(prevPageNumber => prevPageNumber + 1)} | ||
| 77 | ><i className='triangle' style={{ position: "relative", left: "5px", transform: "rotate(180deg)" }}></i> </button> | ||
| 78 | </div> | ||
| 78 | </div> | 79 | </div> |
| 79 | </div> | 80 | </div> |
| 80 | </div> | 81 | <hr /> |
| 81 | <hr /> | 82 | <div id='leaderboard-records'> |
| 82 | <div id='leaderboard-records'> | 83 | {data.records.map((r, index) => ( |
| 83 | {data.records.map((r, index) => ( | 84 | <span className='leaderboard-record' key={index} |
| 84 | <span className='leaderboard-record' key={index} | 85 | style={data.map.is_coop ? { gridTemplateColumns: "3% 4.5% 40% 4% 3.5% 15% 15% 14.5%" } : { gridTemplateColumns: "3% 4.5% 30% 4% 6% 20% 17% 15%" }} |
| 85 | style={data.map.is_coop ? { gridTemplateColumns: "3% 4.5% 40% 4% 3.5% 15% 15% 14.5%" } : { gridTemplateColumns: "3% 4.5% 30% 4% 6% 20% 17% 15%" }} | 86 | > |
| 86 | > | 87 | <span>{r.placement}</span> |
| 87 | <span>{r.placement}</span> | 88 | <span> </span> |
| 88 | <span> </span> | 89 | {r.kind === "multiplayer" ? ( |
| 89 | {r.kind === "multiplayer" ? ( | 90 | <div> |
| 90 | <div> | ||
| 91 | <Link to={`/users/${r.host.steam_id}`}><span><img src={r.host.avatar_link} alt='' /> {r.host.user_name}</span></Link> | 91 | <Link to={`/users/${r.host.steam_id}`}><span><img src={r.host.avatar_link} alt='' /> {r.host.user_name}</span></Link> |
| 92 | <Link to={`/users/${r.partner.steam_id}`}><span><img src={r.partner.avatar_link} alt='' /> {r.partner.user_name}</span></Link> | 92 | <Link to={`/users/${r.partner.steam_id}`}><span><img src={r.partner.avatar_link} alt='' /> {r.partner.user_name}</span></Link> |
| 93 | </div> | 93 | </div> |
| 94 | ) : r.kind === "singleplayer" && ( | 94 | ) : r.kind === "singleplayer" && ( |
| 95 | <div> | 95 | <div> |
| 96 | <Link to={`/users/${r.user.steam_id}`}><span><img src={r.user.avatar_link} alt='' /> {r.user.user_name}</span></Link> | 96 | <Link to={`/users/${r.user.steam_id}`}><span><img src={r.user.avatar_link} alt='' /> {r.user.user_name}</span></Link> |
| 97 | </div> | 97 | </div> |
| 98 | )} | 98 | )} |
| 99 | 99 | ||
| 100 | <span>{r.score_count}</span> | 100 | <span>{r.score_count}</span> |
| 101 | <span> </span> | 101 | <span> </span> |
| 102 | <span className='hover-popup' popup-text={(r.score_time) + " ticks"}>{ticks_to_time(r.score_time)}</span> | 102 | <span className='hover-popup' popup-text={(r.score_time) + " ticks"}>{ticks_to_time(r.score_time)}</span> |
| 103 | <span className='hover-popup' popup-text={r.record_date.replace("T", ' ').split(".")[0]}>{time_ago(new Date(r.record_date.replace("T", " ").replace("Z", "")))}</span> | 103 | <span className='hover-popup' popup-text={r.record_date.replace("T", " ").split(".")[0]}>{time_ago(new Date(r.record_date.replace("T", " ").replace("Z", "")))}</span> |
| 104 | 104 | ||
| 105 | {r.kind === "multiplayer" ? ( | 105 | {r.kind === "multiplayer" ? ( |
| 106 | <span> | 106 | <span> |
| 107 | <button onClick={() => { message("Demo Information", `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | 107 | <button onClick={() => { message("Demo Information", `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> |
| 108 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.partner_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(160deg) contrast(60%) saturate(1000%)" }} /></button> | 108 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.partner_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(160deg) contrast(60%) saturate(1000%)" }} /></button> |
| 109 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.host_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(300deg) contrast(60%) saturate(1000%)" }} /></button> | 109 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.host_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(300deg) contrast(60%) saturate(1000%)" }} /></button> |
| 110 | </span> | 110 | </span> |
| 111 | ) : r.kind === "singleplayer" && ( | 111 | ) : r.kind === "singleplayer" && ( |
| 112 | 112 | ||
| 113 | <span> | 113 | <span> |
| 114 | <button onClick={() => { message("Demo Information", `Demo ID: ${r.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | 114 | <button onClick={() => { message("Demo Information", `Demo ID: ${r.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> |
| 115 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | 115 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.demo_id}`}><img src={DownloadIcon} alt="download" /></button> |
| 116 | </span> | 116 | </span> |
| 117 | )} | 117 | )} |
| 118 | </span> | 118 | </span> |
| 119 | ))} | 119 | ))} |
| 120 | </div> | 120 | </div> |
| 121 | </section> | 121 | </section> |
| 122 | </div> | 122 | </div> |
| 123 | ); | 123 | ); |
| 124 | }; | 124 | }; |
| 125 | 125 | ||
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index f1628b2..89a37e1 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx | |||
| @@ -1,9 +1,9 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link, useNavigate } from 'react-router-dom'; | 2 | import { Link, useNavigate } from "react-router-dom"; |
| 3 | 3 | ||
| 4 | import { ExitIcon, UserIcon, LoginIcon } from '@images/Images'; | 4 | import { ExitIcon, UserIcon, LoginIcon } from "@images/Images"; |
| 5 | import { UserProfile } from '@customTypes/Profile'; | 5 | import { UserProfile } from "@customTypes/Profile"; |
| 6 | import { API } from '@api/Api'; | 6 | import { API } from "@api/Api"; |
| 7 | import "@css/Login.css"; | 7 | import "@css/Login.css"; |
| 8 | 8 | ||
| 9 | interface LoginProps { | 9 | interface LoginProps { |
diff --git a/frontend/src/components/MapEntry.tsx b/frontend/src/components/MapEntry.tsx index 0f494ad..88137d8 100644 --- a/frontend/src/components/MapEntry.tsx +++ b/frontend/src/components/MapEntry.tsx | |||
| @@ -1,12 +1,12 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link } from "react-router-dom"; | 2 | import { Link } from "react-router-dom"; |
| 3 | 3 | ||
| 4 | const MapEntry: React.FC = () => { | 4 | const MapEntry: React.FC = () => { |
| 5 | return ( | 5 | return ( |
| 6 | <div> | 6 | <div> |
| 7 | 7 | ||
| 8 | </div> | 8 | </div> |
| 9 | ) | 9 | ) |
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | export default MapEntry; | 12 | export default MapEntry; |
diff --git a/frontend/src/components/MessageDialog.tsx b/frontend/src/components/MessageDialog.tsx index 5c85189..ae95f8d 100644 --- a/frontend/src/components/MessageDialog.tsx +++ b/frontend/src/components/MessageDialog.tsx | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | 2 | ||
| 3 | import "@css/Dialog.css" | 3 | import "@css/Dialog.css" |
| 4 | 4 | ||
| @@ -9,21 +9,21 @@ interface MessageDialogProps { | |||
| 9 | }; | 9 | }; |
| 10 | 10 | ||
| 11 | const MessageDialog: React.FC<MessageDialogProps> = ({ title, subtitle, onClose }) => { | 11 | const MessageDialog: React.FC<MessageDialogProps> = ({ title, subtitle, onClose }) => { |
| 12 | return ( | 12 | return ( |
| 13 | <div className='dimmer'> | 13 | <div className='dimmer'> |
| 14 | <div className='dialog'> | 14 | <div className='dialog'> |
| 15 | <div className='dialog-element dialog-header'> | 15 | <div className='dialog-element dialog-header'> |
| 16 | <span>{title}</span> | 16 | <span>{title}</span> |
| 17 | </div> | ||
| 18 | <div className='dialog-element dialog-description'> | ||
| 19 | <span>{subtitle}</span> | ||
| 20 | </div> | ||
| 21 | <div className='dialog-element dialog-btns-container'> | ||
| 22 | <button onClick={onClose}>Close</button> | ||
| 23 | </div> | ||
| 24 | </div> | ||
| 25 | </div> | 17 | </div> |
| 26 | ) | 18 | <div className='dialog-element dialog-description'> |
| 19 | <span>{subtitle}</span> | ||
| 20 | </div> | ||
| 21 | <div className='dialog-element dialog-btns-container'> | ||
| 22 | <button onClick={onClose}>Close</button> | ||
| 23 | </div> | ||
| 24 | </div> | ||
| 25 | </div> | ||
| 26 | ) | ||
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | export default MessageDialog; | 29 | export default MessageDialog; |
diff --git a/frontend/src/components/MessageDialogLoad.tsx b/frontend/src/components/MessageDialogLoad.tsx index 966e064..000a2ab 100644 --- a/frontend/src/components/MessageDialogLoad.tsx +++ b/frontend/src/components/MessageDialogLoad.tsx | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | 2 | ||
| 3 | import "@css/Dialog.css" | 3 | import "@css/Dialog.css" |
| 4 | 4 | ||
| @@ -8,22 +8,22 @@ interface MessageDialogLoadProps { | |||
| 8 | }; | 8 | }; |
| 9 | 9 | ||
| 10 | const MessageDialogLoad: React.FC<MessageDialogLoadProps> = ({ title, onClose }) => { | 10 | const MessageDialogLoad: React.FC<MessageDialogLoadProps> = ({ title, onClose }) => { |
| 11 | return ( | 11 | return ( |
| 12 | <div className='dimmer'> | 12 | <div className='dimmer'> |
| 13 | <div className='dialog'> | 13 | <div className='dialog'> |
| 14 | <div className='dialog-element dialog-header'> | 14 | <div className='dialog-element dialog-header'> |
| 15 | <span>{title}</span> | 15 | <span>{title}</span> |
| 16 | </div> | 16 | </div> |
| 17 | <div className='dialog-element dialog-description'> | 17 | <div className='dialog-element dialog-description'> |
| 18 | <div style={{display: "flex", justifyContent: "center"}}> | 18 | <div style={{display: "flex", justifyContent: "center"}}> |
| 19 | <span className="loader"></span> | 19 | <span className="loader"></span> |
| 20 | </div> | 20 | </div> |
| 21 | </div> | 21 | </div> |
| 22 | <div className='dialog-element dialog-btns-container'> | 22 | <div className='dialog-element dialog-btns-container'> |
| 23 | </div> | ||
| 24 | </div> | ||
| 25 | </div> | 23 | </div> |
| 26 | ) | 24 | </div> |
| 25 | </div> | ||
| 26 | ) | ||
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | export default MessageDialogLoad; | 29 | export default MessageDialogLoad; |
diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx index 925b8a8..19ce0ce 100644 --- a/frontend/src/components/ModMenu.tsx +++ b/frontend/src/components/ModMenu.tsx | |||
| @@ -1,12 +1,12 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import ReactMarkdown from 'react-markdown'; | 2 | import ReactMarkdown from "react-markdown"; |
| 3 | import { useNavigate } from 'react-router-dom'; | 3 | import { useNavigate } from "react-router-dom"; |
| 4 | 4 | ||
| 5 | import { MapSummary } from '@customTypes/Map'; | 5 | import { MapSummary } from "@customTypes/Map"; |
| 6 | import { ModMenuContent } from '@customTypes/Content'; | 6 | import { ModMenuContent } from "@customTypes/Content"; |
| 7 | import { API } from '@api/Api'; | 7 | import { API } from "@api/Api"; |
| 8 | import "@css/ModMenu.css" | 8 | import "@css/ModMenu.css" |
| 9 | import useConfirm from '@hooks/UseConfirm'; | 9 | import useConfirm from "@hooks/UseConfirm"; |
| 10 | 10 | ||
| 11 | interface ModMenuProps { | 11 | interface ModMenuProps { |
| 12 | token?: string; | 12 | token?: string; |
| @@ -55,10 +55,10 @@ const ModMenu: React.FC<ModMenuProps> = ({ token, data, selectedRun, mapID }) => | |||
| 55 | width *= 320 / height; | 55 | width *= 320 / height; |
| 56 | height = 320; | 56 | height = 320; |
| 57 | } | 57 | } |
| 58 | const canvas = document.createElement('canvas'); | 58 | const canvas = document.createElement("canvas"); |
| 59 | canvas.width = width; | 59 | canvas.width = width; |
| 60 | canvas.height = height; | 60 | canvas.height = height; |
| 61 | canvas.getContext('2d')!.drawImage(img, 0, 0, width, height); | 61 | canvas.getContext("2d")!.drawImage(img, 0, 0, width, height); |
| 62 | resolve(canvas.toDataURL(file.type, 0.6)); | 62 | resolve(canvas.toDataURL(file.type, 0.6)); |
| 63 | }; | 63 | }; |
| 64 | } | 64 | } |
diff --git a/frontend/src/components/RankingEntry.tsx b/frontend/src/components/RankingEntry.tsx index b899965..9ad9e1c 100644 --- a/frontend/src/components/RankingEntry.tsx +++ b/frontend/src/components/RankingEntry.tsx | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link } from "react-router-dom"; | 2 | import { Link } from "react-router-dom"; |
| 3 | import { RankingType, SteamRanking, SteamRankingType } from '@customTypes/Ranking'; | 3 | import { RankingType, SteamRankingType } from "@customTypes/Ranking"; |
| 4 | 4 | ||
| 5 | enum RankingCategories { | 5 | enum RankingCategories { |
| 6 | rankings_overall, | 6 | rankings_overall, |
| @@ -14,33 +14,33 @@ interface RankingEntryProps { | |||
| 14 | }; | 14 | }; |
| 15 | 15 | ||
| 16 | const RankingEntry: React.FC<RankingEntryProps> = (prop) => { | 16 | const RankingEntry: React.FC<RankingEntryProps> = (prop) => { |
| 17 | if ("placement" in prop.curRankingData) { | 17 | if ("placement" in prop.curRankingData) { |
| 18 | return ( | 18 | return ( |
| 19 | <div className='leaderboard-entry'> | 19 | <div className='leaderboard-entry'> |
| 20 | <span>{prop.curRankingData.placement}</span> | 20 | <span>{prop.curRankingData.placement}</span> |
| 21 | <div> | 21 | <div> |
| 22 | <Link to={`/users/${prop.curRankingData.user.steam_id}`}> | 22 | <Link to={`/users/${prop.curRankingData.user.steam_id}`}> |
| 23 | <img src={prop.curRankingData.user.avatar_link}></img> | 23 | <img src={prop.curRankingData.user.avatar_link}></img> |
| 24 | <span>{prop.curRankingData.user.user_name}</span> | 24 | <span>{prop.curRankingData.user.user_name}</span> |
| 25 | </Link> | 25 | </Link> |
| 26 | </div> | 26 | </div> |
| 27 | <span>{prop.curRankingData.total_score}</span> | 27 | <span>{prop.curRankingData.total_score}</span> |
| 28 | </div> | 28 | </div> |
| 29 | ) | 29 | ) |
| 30 | } else { | 30 | } else { |
| 31 | return ( | 31 | return ( |
| 32 | <div className='leaderboard-entry'> | 32 | <div className='leaderboard-entry'> |
| 33 | <span>{prop.currentLeaderboardType == RankingCategories.rankings_singleplayer ? prop.curRankingData.sp_rank : prop.currentLeaderboardType == RankingCategories.rankings_multiplayer ? prop.curRankingData.mp_rank : prop.curRankingData.overall_rank}</span> | 33 | <span>{prop.currentLeaderboardType == RankingCategories.rankings_singleplayer ? prop.curRankingData.sp_rank : prop.currentLeaderboardType == RankingCategories.rankings_multiplayer ? prop.curRankingData.mp_rank : prop.curRankingData.overall_rank}</span> |
| 34 | <div> | 34 | <div> |
| 35 | <Link to={`/users/${prop.curRankingData.steam_id}`}> | 35 | <Link to={`/users/${prop.curRankingData.steam_id}`}> |
| 36 | <img src={prop.curRankingData.avatar_link}></img> | 36 | <img src={prop.curRankingData.avatar_link}></img> |
| 37 | <span>{prop.curRankingData.user_name}</span> | 37 | <span>{prop.curRankingData.user_name}</span> |
| 38 | </Link> | 38 | </Link> |
| 39 | </div> | 39 | </div> |
| 40 | <span>{prop.currentLeaderboardType == RankingCategories.rankings_singleplayer ? prop.curRankingData.sp_score : prop.currentLeaderboardType == RankingCategories.rankings_multiplayer ? prop.curRankingData.mp_score : prop.curRankingData.overall_score}</span> | 40 | <span>{prop.currentLeaderboardType == RankingCategories.rankings_singleplayer ? prop.curRankingData.sp_score : prop.currentLeaderboardType == RankingCategories.rankings_multiplayer ? prop.curRankingData.mp_score : prop.curRankingData.overall_score}</span> |
| 41 | </div> | 41 | </div> |
| 42 | ) | 42 | ) |
| 43 | } | 43 | } |
| 44 | } | 44 | } |
| 45 | 45 | ||
| 46 | export default RankingEntry; | 46 | export default RankingEntry; |
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index f972c6f..eef5bca 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx | |||
| @@ -1,11 +1,11 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link, useLocation } from 'react-router-dom'; | 2 | import { Link, useLocation } from "react-router-dom"; |
| 3 | 3 | ||
| 4 | import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images'; | 4 | import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from "@images/Images"; |
| 5 | import Login from '@components/Login'; | 5 | import Login from "@components/Login"; |
| 6 | import { UserProfile } from '@customTypes/Profile'; | 6 | import { UserProfile } from "@customTypes/Profile"; |
| 7 | import { Search } from '@customTypes/Search'; | 7 | import { Search } from "@customTypes/Search"; |
| 8 | import { API } from '@api/Api'; | 8 | import { API } from "@api/Api"; |
| 9 | import "@css/Sidebar.css"; | 9 | import "@css/Sidebar.css"; |
| 10 | 10 | ||
| 11 | interface SidebarProps { | 11 | interface SidebarProps { |
| @@ -39,7 +39,7 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo | |||
| 39 | }; | 39 | }; |
| 40 | 40 | ||
| 41 | const _handle_sidebar_hide = () => { | 41 | const _handle_sidebar_hide = () => { |
| 42 | var btn = document.querySelectorAll("button.sidebar-button") as NodeListOf<HTMLElement> | 42 | const btn = document.querySelectorAll("button.sidebar-button") as NodeListOf<HTMLElement> |
| 43 | const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement> | 43 | const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement> |
| 44 | const side = document.querySelector("#sidebar-list") as HTMLElement; | 44 | const side = document.querySelector("#sidebar-list") as HTMLElement; |
| 45 | const searchbar = document.querySelector("#searchbar") as HTMLInputElement; | 45 | const searchbar = document.querySelector("#searchbar") as HTMLInputElement; |
| @@ -140,13 +140,13 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo | |||
| 140 | </div> | 140 | </div> |
| 141 | </div> | 141 | </div> |
| 142 | </Link> | 142 | </Link> |
| 143 | <button id='hamburger-menu' onClick={_toggle_mobile_menu} className={isMobileMenuOpen ? 'open' : ''}> | 143 | <button id='hamburger-menu' onClick={_toggle_mobile_menu} className={isMobileMenuOpen ? "open" : ""}> |
| 144 | <span></span> | 144 | <span></span> |
| 145 | <span></span> | 145 | <span></span> |
| 146 | <span></span> | 146 | <span></span> |
| 147 | </button> | 147 | </button> |
| 148 | </div> | 148 | </div> |
| 149 | <div id='sidebar' className={isMobileMenuOpen ? 'mobile-open' : ''}> | 149 | <div id='sidebar' className={isMobileMenuOpen ? "mobile-open" : ""}> |
| 150 | <Link to="/" tabIndex={-1}> | 150 | <Link to="/" tabIndex={-1}> |
| 151 | <div id='logo'> {/* logo */} | 151 | <div id='logo'> {/* logo */} |
| 152 | <img src={LogoIcon} alt="" height={"80px"} /> | 152 | <img src={LogoIcon} alt="" height={"80px"} /> |
| @@ -204,7 +204,7 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo | |||
| 204 | </Link> | 204 | </Link> |
| 205 | </div> | 205 | </div> |
| 206 | </div> | 206 | </div> |
| 207 | <div id='search-panel' className={isMobileSearchOpen ? 'mobile-search-open' : ''}> | 207 | <div id='search-panel' className={isMobileSearchOpen ? "mobile-search-open" : ""}> |
| 208 | <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} /> | 208 | <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} /> |
| 209 | <div className='mobile-search-header'> | 209 | <div className='mobile-search-header'> |
| 210 | <button className='mobile-search-close' onClick={_close_mobile_search}>✕</button> | 210 | <button className='mobile-search-close' onClick={_close_mobile_search}>✕</button> |
| @@ -220,15 +220,15 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo | |||
| 220 | </Link> | 220 | </Link> |
| 221 | ))} | 221 | ))} |
| 222 | {searchData?.players.map((q, index) => | 222 | {searchData?.players.map((q, index) => |
| 223 | ( | 223 | ( |
| 224 | <Link to={ | 224 | <Link to={ |
| 225 | profile && q.steam_id === profile.steam_id ? `/profile` : | 225 | profile && q.steam_id === profile.steam_id ? "/profile" : |
| 226 | `/users/${q.steam_id}` | 226 | `/users/${q.steam_id}` |
| 227 | } className='search-player' key={index} onClick={_close_mobile_search}> | 227 | } className='search-player' key={index} onClick={_close_mobile_search}> |
| 228 | <img src={q.avatar_link} alt='pfp'></img> | 228 | <img src={q.avatar_link} alt='pfp'></img> |
| 229 | <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span> | 229 | <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span> |
| 230 | </Link> | 230 | </Link> |
| 231 | ))} | 231 | ))} |
| 232 | 232 | ||
| 233 | </div> | 233 | </div> |
| 234 | </div> | 234 | </div> |
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx index 7da2f1e..72b5bf0 100644 --- a/frontend/src/components/Summary.tsx +++ b/frontend/src/components/Summary.tsx | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import ReactMarkdown from 'react-markdown'; | 2 | import ReactMarkdown from "react-markdown"; |
| 3 | 3 | ||
| 4 | import { MapSummary } from '@customTypes/Map'; | 4 | import { MapSummary } from "@customTypes/Map"; |
| 5 | import "@css/Maps.css" | 5 | import "@css/Maps.css" |
| 6 | 6 | ||
| 7 | interface SummaryProps { | 7 | interface SummaryProps { |
| @@ -16,7 +16,7 @@ const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data }) | |||
| 16 | const [historySelected, setHistorySelected] = React.useState<boolean>(false); | 16 | const [historySelected, setHistorySelected] = React.useState<boolean>(false); |
| 17 | 17 | ||
| 18 | function _select_run(idx: number, category_id: number) { | 18 | function _select_run(idx: number, category_id: number) { |
| 19 | let r = document.querySelectorAll("button.record"); | 19 | const r = document.querySelectorAll("button.record"); |
| 20 | r.forEach(e => (e as HTMLElement).style.backgroundColor = "#2b2e46"); | 20 | r.forEach(e => (e as HTMLElement).style.backgroundColor = "#2b2e46"); |
| 21 | (r[idx] as HTMLElement).style.backgroundColor = "#161723" | 21 | (r[idx] as HTMLElement).style.backgroundColor = "#161723" |
| 22 | 22 | ||
| @@ -66,7 +66,7 @@ const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data }) | |||
| 66 | style={data.map.image === "" ? { backgroundColor: "#202232" } : {}}> | 66 | style={data.map.image === "" ? { backgroundColor: "#202232" } : {}}> |
| 67 | <img src={data.map.image} alt="" id='category-image'></img> | 67 | <img src={data.map.image} alt="" id='category-image'></img> |
| 68 | <p><span className='portal-count'>{data.summary.routes[selectedRun].history.score_count}</span> | 68 | <p><span className='portal-count'>{data.summary.routes[selectedRun].history.score_count}</span> |
| 69 | {data.summary.routes[selectedRun].history.score_count === 1 ? ` portal` : ` portals`}</p> | 69 | {data.summary.routes[selectedRun].history.score_count === 1 ? " portal" : " portals"}</p> |
| 70 | {data.map.is_coop ? // TODO: make this part dynamic | 70 | {data.map.is_coop ? // TODO: make this part dynamic |
| 71 | ( | 71 | ( |
| 72 | <span style={{ gridTemplateColumns: "1fr 1fr 1fr" }}> | 72 | <span style={{ gridTemplateColumns: "1fr 1fr 1fr" }}> |
| @@ -109,7 +109,7 @@ const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data }) | |||
| 109 | _select_run(index, r.category.id); | 109 | _select_run(index, r.category.id); |
| 110 | }}> | 110 | }}> |
| 111 | <span>{new Date(r.history.date).toLocaleDateString( | 111 | <span>{new Date(r.history.date).toLocaleDateString( |
| 112 | "en-US", { month: 'long', day: 'numeric', year: 'numeric' } | 112 | "en-US", { month: "long", day: "numeric", year: "numeric" } |
| 113 | )}</span> | 113 | )}</span> |
| 114 | <span>{r.history.score_count}</span> | 114 | <span>{r.history.score_count}</span> |
| 115 | <span>{r.history.runner_name}</span> | 115 | <span>{r.history.runner_name}</span> |
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx index c02fdb8..dd609a1 100644 --- a/frontend/src/components/UploadRunDialog.tsx +++ b/frontend/src/components/UploadRunDialog.tsx | |||
| @@ -1,15 +1,15 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { UploadRunContent } from '@customTypes/Content'; | 2 | import { UploadRunContent } from "@customTypes/Content"; |
| 3 | import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from '@nekz/sdp'; | 3 | import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from "@nekz/sdp"; |
| 4 | 4 | ||
| 5 | import '@css/UploadRunDialog.css'; | 5 | import "@css/UploadRunDialog.css"; |
| 6 | import { Game } from '@customTypes/Game'; | 6 | import { Game } from "@customTypes/Game"; |
| 7 | import { API } from '@api/Api'; | 7 | import { API } from "@api/Api"; |
| 8 | import { useNavigate } from 'react-router-dom'; | 8 | import { useNavigate } from "react-router-dom"; |
| 9 | import useMessage from '@hooks/UseMessage'; | 9 | import useMessage from "@hooks/UseMessage"; |
| 10 | import useConfirm from '@hooks/UseConfirm'; | 10 | import useConfirm from "@hooks/UseConfirm"; |
| 11 | import useMessageLoad from "@hooks/UseMessageLoad"; | 11 | import useMessageLoad from "@hooks/UseMessageLoad"; |
| 12 | import { MapNames } from '@customTypes/MapNames'; | 12 | import { MapNames } from "@customTypes/MapNames"; |
| 13 | 13 | ||
| 14 | interface UploadRunDialogProps { | 14 | interface UploadRunDialogProps { |
| 15 | token?: string; | 15 | token?: string; |
diff --git a/frontend/src/hooks/UseConfirm.tsx b/frontend/src/hooks/UseConfirm.tsx index e86d70d..593427e 100644 --- a/frontend/src/hooks/UseConfirm.tsx +++ b/frontend/src/hooks/UseConfirm.tsx | |||
| @@ -1,40 +1,40 @@ | |||
| 1 | import React, { useState } from 'react'; | 1 | import React, { useState } from "react"; |
| 2 | import ConfirmDialog from '@components/ConfirmDialog'; | 2 | import ConfirmDialog from "@components/ConfirmDialog"; |
| 3 | 3 | ||
| 4 | const useConfirm = () => { | 4 | const useConfirm = () => { |
| 5 | const [isOpen, setIsOpen] = useState(false); | 5 | const [isOpen, setIsOpen] = useState(false); |
| 6 | const [title, setTitle] = useState<string>(""); | 6 | const [title, setTitle] = useState<string>(""); |
| 7 | const [subtitle, setSubtitle] = useState<string>(""); | 7 | const [subtitle, setSubtitle] = useState<string>(""); |
| 8 | const [resolvePromise, setResolvePromise] = useState<((value: boolean) => void) | null>(null); | 8 | const [resolvePromise, setResolvePromise] = useState<((value: boolean) => void) | null>(null); |
| 9 | 9 | ||
| 10 | const confirm = ( titleN: string, subtitleN: string ) => { | 10 | const confirm = ( titleN: string, subtitleN: string ) => { |
| 11 | setIsOpen(true); | 11 | setIsOpen(true); |
| 12 | setTitle(titleN); | 12 | setTitle(titleN); |
| 13 | setSubtitle(subtitleN); | 13 | setSubtitle(subtitleN); |
| 14 | return new Promise<boolean>((resolve) => { | 14 | return new Promise<boolean>((resolve) => { |
| 15 | setResolvePromise(() => resolve); | 15 | setResolvePromise(() => resolve); |
| 16 | }); | 16 | }); |
| 17 | }; | 17 | }; |
| 18 | 18 | ||
| 19 | const handleConfirm = () => { | 19 | const handleConfirm = () => { |
| 20 | setIsOpen(false); | 20 | setIsOpen(false); |
| 21 | if (resolvePromise) { | 21 | if (resolvePromise) { |
| 22 | resolvePromise(true); | 22 | resolvePromise(true); |
| 23 | } | ||
| 24 | } | 23 | } |
| 24 | } | ||
| 25 | 25 | ||
| 26 | const handleCancel = () => { | 26 | const handleCancel = () => { |
| 27 | setIsOpen(false); | 27 | setIsOpen(false); |
| 28 | if (resolvePromise) { | 28 | if (resolvePromise) { |
| 29 | resolvePromise(false); | 29 | resolvePromise(false); |
| 30 | } | ||
| 31 | } | 30 | } |
| 31 | } | ||
| 32 | 32 | ||
| 33 | const ConfirmDialogComponent = isOpen && ( | 33 | const ConfirmDialogComponent = isOpen && ( |
| 34 | <ConfirmDialog title={title} subtitle={subtitle} onConfirm={handleConfirm} onCancel={handleCancel}></ConfirmDialog> | 34 | <ConfirmDialog title={title} subtitle={subtitle} onConfirm={handleConfirm} onCancel={handleCancel}></ConfirmDialog> |
| 35 | ); | 35 | ); |
| 36 | 36 | ||
| 37 | return { confirm, ConfirmDialogComponent }; | 37 | return { confirm, ConfirmDialogComponent }; |
| 38 | } | 38 | } |
| 39 | 39 | ||
| 40 | export default useConfirm; | 40 | export default useConfirm; |
diff --git a/frontend/src/hooks/UseMessage.tsx b/frontend/src/hooks/UseMessage.tsx index 97ec746..22a5168 100644 --- a/frontend/src/hooks/UseMessage.tsx +++ b/frontend/src/hooks/UseMessage.tsx | |||
| @@ -1,37 +1,37 @@ | |||
| 1 | import React, { useState } from 'react'; | 1 | import React, { useState } from "react"; |
| 2 | import MessageDialog from "@components/MessageDialog"; | 2 | import MessageDialog from "@components/MessageDialog"; |
| 3 | 3 | ||
| 4 | const useMessage = () => { | 4 | const useMessage = () => { |
| 5 | const [isOpen, setIsOpen] = useState(false); | 5 | const [isOpen, setIsOpen] = useState(false); |
| 6 | 6 | ||
| 7 | const [title, setTitle] = useState<string>(""); | 7 | const [title, setTitle] = useState<string>(""); |
| 8 | const [subtitle, setSubtitle] = useState<string>(""); | 8 | const [subtitle, setSubtitle] = useState<string>(""); |
| 9 | const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(null); | 9 | const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(null); |
| 10 | 10 | ||
| 11 | const message = (title: string, subtitle: string) => { | 11 | const message = (title: string, subtitle: string) => { |
| 12 | setIsOpen(true); | 12 | setIsOpen(true); |
| 13 | setTitle(title); | 13 | setTitle(title); |
| 14 | setSubtitle(subtitle); | 14 | setSubtitle(subtitle); |
| 15 | return new Promise((resolve) => { | 15 | return new Promise((resolve) => { |
| 16 | setResolvePromise(() => resolve); | 16 | setResolvePromise(() => resolve); |
| 17 | }); | 17 | }); |
| 18 | }; | 18 | }; |
| 19 | 19 | ||
| 20 | const handleClose = () => { | 20 | const handleClose = () => { |
| 21 | setIsOpen(false); | 21 | setIsOpen(false); |
| 22 | if (resolvePromise) { | 22 | if (resolvePromise) { |
| 23 | resolvePromise(); | 23 | resolvePromise(); |
| 24 | setResolvePromise(null); | 24 | setResolvePromise(null); |
| 25 | } | 25 | } |
| 26 | }; | 26 | }; |
| 27 | 27 | ||
| 28 | const MessageDialogComponent = isOpen && ( | 28 | const MessageDialogComponent = isOpen && ( |
| 29 | <div className="dialog-container"> | 29 | <div className="dialog-container"> |
| 30 | <MessageDialog title={title} subtitle={subtitle} onClose={handleClose}></MessageDialog> | 30 | <MessageDialog title={title} subtitle={subtitle} onClose={handleClose}></MessageDialog> |
| 31 | </div> | 31 | </div> |
| 32 | ); | 32 | ); |
| 33 | 33 | ||
| 34 | return { message, MessageDialogComponent }; | 34 | return { message, MessageDialogComponent }; |
| 35 | } | 35 | } |
| 36 | 36 | ||
| 37 | export default useMessage; | 37 | export default useMessage; |
diff --git a/frontend/src/hooks/UseMessageLoad.tsx b/frontend/src/hooks/UseMessageLoad.tsx index 228c2b4..eb42a45 100644 --- a/frontend/src/hooks/UseMessageLoad.tsx +++ b/frontend/src/hooks/UseMessageLoad.tsx | |||
| @@ -1,35 +1,35 @@ | |||
| 1 | import React, { useState } from 'react'; | 1 | import React, { useState } from "react"; |
| 2 | import MessageDialogLoad from "@components/MessageDialogLoad"; | 2 | import MessageDialogLoad from "@components/MessageDialogLoad"; |
| 3 | 3 | ||
| 4 | const useMessageLoad = () => { | 4 | const useMessageLoad = () => { |
| 5 | const [isOpen, setIsOpen] = useState(false); | 5 | const [isOpen, setIsOpen] = useState(false); |
| 6 | 6 | ||
| 7 | const [title, setTitle] = useState<string>(""); | 7 | const [title, setTitle] = useState<string>(""); |
| 8 | const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(null); | 8 | const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(null); |
| 9 | 9 | ||
| 10 | const messageLoad = (title: string) => { | 10 | const messageLoad = (title: string) => { |
| 11 | setIsOpen(true); | 11 | setIsOpen(true); |
| 12 | setTitle(title); | 12 | setTitle(title); |
| 13 | return new Promise((resolve) => { | 13 | return new Promise((resolve) => { |
| 14 | setResolvePromise(() => resolve); | 14 | setResolvePromise(() => resolve); |
| 15 | }); | 15 | }); |
| 16 | }; | 16 | }; |
| 17 | 17 | ||
| 18 | const messageLoadClose = () => { | 18 | const messageLoadClose = () => { |
| 19 | setIsOpen(false); | 19 | setIsOpen(false); |
| 20 | if (resolvePromise) { | 20 | if (resolvePromise) { |
| 21 | resolvePromise(); | 21 | resolvePromise(); |
| 22 | setResolvePromise(null); | 22 | setResolvePromise(null); |
| 23 | } | 23 | } |
| 24 | }; | 24 | }; |
| 25 | 25 | ||
| 26 | const MessageDialogLoadComponent = isOpen && ( | 26 | const MessageDialogLoadComponent = isOpen && ( |
| 27 | <div className="dialog-container"> | 27 | <div className="dialog-container"> |
| 28 | <MessageDialogLoad title={title} onClose={messageLoadClose}></MessageDialogLoad> | 28 | <MessageDialogLoad title={title} onClose={messageLoadClose}></MessageDialogLoad> |
| 29 | </div> | 29 | </div> |
| 30 | ); | 30 | ); |
| 31 | 31 | ||
| 32 | return { messageLoad, messageLoadClose, MessageDialogLoadComponent }; | 32 | return { messageLoad, messageLoadClose, MessageDialogLoadComponent }; |
| 33 | } | 33 | } |
| 34 | 34 | ||
| 35 | export default useMessageLoad; | 35 | export default useMessageLoad; |
diff --git a/frontend/src/images/Images.tsx b/frontend/src/images/Images.tsx index 198431b..c4511ef 100644 --- a/frontend/src/images/Images.tsx +++ b/frontend/src/images/Images.tsx | |||
| @@ -1,25 +1,25 @@ | |||
| 1 | import logo from "./png/logo.png" | 1 | import logo from "./png/logo.png" |
| 2 | import login from "./png/login.png" | 2 | import login from "./png/login.png" |
| 3 | import img1 from './png/1.png'; | 3 | import img1 from "./png/1.png"; |
| 4 | import img2 from './png/2.png'; | 4 | import img2 from "./png/2.png"; |
| 5 | import img3 from './png/3.png'; | 5 | import img3 from "./png/3.png"; |
| 6 | import img4 from './png/4.png'; | 6 | import img4 from "./png/4.png"; |
| 7 | import img5 from './png/5.png'; | 7 | import img5 from "./png/5.png"; |
| 8 | import img6 from './png/6.png'; | 8 | import img6 from "./png/6.png"; |
| 9 | import img7 from './png/7.png'; | 9 | import img7 from "./png/7.png"; |
| 10 | import img8 from './png/8.png'; | 10 | import img8 from "./png/8.png"; |
| 11 | import img9 from './png/9.png'; | 11 | import img9 from "./png/9.png"; |
| 12 | import img10 from './png/10.png'; | 12 | import img10 from "./png/10.png"; |
| 13 | import img11 from './png/11.png'; | 13 | import img11 from "./png/11.png"; |
| 14 | import img12 from './png/12.png'; | 14 | import img12 from "./png/12.png"; |
| 15 | import img13 from './png/13.png'; | 15 | import img13 from "./png/13.png"; |
| 16 | import img14 from './png/14.png'; | 16 | import img14 from "./png/14.png"; |
| 17 | import img15 from './png/15.png'; | 17 | import img15 from "./png/15.png"; |
| 18 | import img16 from './png/16.png'; | 18 | import img16 from "./png/16.png"; |
| 19 | import img17 from './png/17.png'; | 19 | import img17 from "./png/17.png"; |
| 20 | import img18 from './png/18.png'; | 20 | import img18 from "./png/18.png"; |
| 21 | import img19 from './png/19.png'; | 21 | import img19 from "./png/19.png"; |
| 22 | import img20 from './png/20.png'; | 22 | import img20 from "./png/20.png"; |
| 23 | import img21 from "./png/21.png"; | 23 | import img21 from "./png/21.png"; |
| 24 | 24 | ||
| 25 | export const LogoIcon = logo; | 25 | export const LogoIcon = logo; |
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index eec2ff4..13d180c 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx | |||
| @@ -1,11 +1,11 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import ReactDOM from 'react-dom/client'; | 2 | import ReactDOM from "react-dom/client"; |
| 3 | import { BrowserRouter } from "react-router-dom"; | 3 | import { BrowserRouter } from "react-router-dom"; |
| 4 | 4 | ||
| 5 | import App from './App'; | 5 | import App from "./App"; |
| 6 | 6 | ||
| 7 | const root = ReactDOM.createRoot( | 7 | const root = ReactDOM.createRoot( |
| 8 | document.getElementById('root') as HTMLElement | 8 | document.getElementById("root") as HTMLElement |
| 9 | ); | 9 | ); |
| 10 | 10 | ||
| 11 | root.render( | 11 | root.render( |
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx index a8b7826..e4abdf4 100644 --- a/frontend/src/pages/About.tsx +++ b/frontend/src/pages/About.tsx | |||
| @@ -1,40 +1,40 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import ReactMarkdown from 'react-markdown'; | 2 | import ReactMarkdown from "react-markdown"; |
| 3 | import { Helmet } from 'react-helmet'; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import '@css/About.css'; | 5 | import "@css/About.css"; |
| 6 | 6 | ||
| 7 | const About: React.FC = () => { | 7 | const About: React.FC = () => { |
| 8 | 8 | ||
| 9 | const [aboutText, setAboutText] = React.useState<string>(""); | 9 | const [aboutText, setAboutText] = React.useState<string>(""); |
| 10 | 10 | ||
| 11 | React.useEffect(() => { | 11 | React.useEffect(() => { |
| 12 | const fetchReadme = async () => { | 12 | const fetchReadme = async () => { |
| 13 | try { | 13 | try { |
| 14 | const response = await fetch( | 14 | const response = await fetch( |
| 15 | 'https://raw.githubusercontent.com/pektezol/lphub/main/README.md' | 15 | "https://raw.githubusercontent.com/pektezol/lphub/main/README.md" |
| 16 | ); | 16 | ); |
| 17 | if (!response.ok) { | 17 | if (!response.ok) { |
| 18 | throw new Error('Failed to fetch README'); | 18 | throw new Error("Failed to fetch README"); |
| 19 | } | 19 | } |
| 20 | const readmeText = await response.text(); | 20 | const readmeText = await response.text(); |
| 21 | setAboutText(readmeText); | 21 | setAboutText(readmeText); |
| 22 | } catch (error) { | 22 | } catch (error) { |
| 23 | console.error('Error fetching README:', error); | 23 | console.error("Error fetching README:", error); |
| 24 | } | 24 | } |
| 25 | }; | 25 | }; |
| 26 | fetchReadme(); | 26 | fetchReadme(); |
| 27 | }, []); | 27 | }, []); |
| 28 | 28 | ||
| 29 | 29 | ||
| 30 | return ( | 30 | return ( |
| 31 | <div id="about"> | 31 | <div id="about"> |
| 32 | <Helmet> | 32 | <Helmet> |
| 33 | <title>LPHUB | About</title> | 33 | <title>LPHUB | About</title> |
| 34 | </Helmet> | 34 | </Helmet> |
| 35 | <ReactMarkdown>{aboutText}</ReactMarkdown> | 35 | <ReactMarkdown>{aboutText}</ReactMarkdown> |
| 36 | </div> | 36 | </div> |
| 37 | ); | 37 | ); |
| 38 | }; | 38 | }; |
| 39 | 39 | ||
| 40 | export default About; | 40 | export default About; |
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx index 15cc891..909ea20 100644 --- a/frontend/src/pages/Games.tsx +++ b/frontend/src/pages/Games.tsx | |||
| @@ -1,46 +1,46 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Helmet } from 'react-helmet'; | 2 | import { Helmet } from "react-helmet"; |
| 3 | 3 | ||
| 4 | import GameEntry from '@components/GameEntry'; | 4 | import GameEntry from "@components/GameEntry"; |
| 5 | import { Game } from '@customTypes/Game'; | 5 | import { Game } from "@customTypes/Game"; |
| 6 | import "@css/Maps.css" | 6 | import "@css/Maps.css" |
| 7 | 7 | ||
| 8 | interface GamesProps { | 8 | interface GamesProps { |
| 9 | games: Game[]; | 9 | games: Game[]; |
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | const Games: React.FC<GamesProps> = ({ games }) => { | 12 | const Games: React.FC<GamesProps> = ({ games }) => { |
| 13 | 13 | ||
| 14 | const _page_load = () => { | 14 | const _page_load = () => { |
| 15 | const loaders = document.querySelectorAll(".loader"); | 15 | const loaders = document.querySelectorAll(".loader"); |
| 16 | loaders.forEach((loader) => { | 16 | loaders.forEach((loader) => { |
| 17 | (loader as HTMLElement).style.display = "none"; | 17 | (loader as HTMLElement).style.display = "none"; |
| 18 | }); | 18 | }); |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | React.useEffect(() => { | 21 | React.useEffect(() => { |
| 22 | document.querySelectorAll(".games-page-item-body").forEach((game, index) => { | 22 | document.querySelectorAll(".games-page-item-body").forEach((game, index) => { |
| 23 | game.innerHTML = ""; | 23 | game.innerHTML = ""; |
| 24 | }); | 24 | }); |
| 25 | _page_load(); | 25 | _page_load(); |
| 26 | }, []); | 26 | }, []); |
| 27 | 27 | ||
| 28 | return ( | 28 | return ( |
| 29 | <div className='games-page'> | 29 | <div className='games-page'> |
| 30 | <Helmet> | 30 | <Helmet> |
| 31 | <title>LPHUB | Games</title> | 31 | <title>LPHUB | Games</title> |
| 32 | </Helmet> | 32 | </Helmet> |
| 33 | <section> | 33 | <section> |
| 34 | <div className='games-page-content'> | 34 | <div className='games-page-content'> |
| 35 | <div className='games-page-item-content'> | 35 | <div className='games-page-item-content'> |
| 36 | {games.map((game, index) => ( | 36 | {games.map((game, index) => ( |
| 37 | <GameEntry game={game} key={index} /> | 37 | <GameEntry game={game} key={index} /> |
| 38 | ))} | 38 | ))} |
| 39 | </div> | 39 | </div> |
| 40 | </div> | ||
| 41 | </section> | ||
| 42 | </div> | 40 | </div> |
| 43 | ); | 41 | </section> |
| 42 | </div> | ||
| 43 | ); | ||
| 44 | }; | 44 | }; |
| 45 | 45 | ||
| 46 | export default Games; | 46 | export default Games; |
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx index 4f46af5..3f30d9a 100644 --- a/frontend/src/pages/Homepage.tsx +++ b/frontend/src/pages/Homepage.tsx | |||
| @@ -1,22 +1,22 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Helmet } from 'react-helmet'; | 2 | import { Helmet } from "react-helmet"; |
| 3 | 3 | ||
| 4 | const Homepage: React.FC = () => { | 4 | const Homepage: React.FC = () => { |
| 5 | 5 | ||
| 6 | return ( | 6 | return ( |
| 7 | <main> | 7 | <main> |
| 8 | <Helmet> | 8 | <Helmet> |
| 9 | <title>LPHUB | Homepage</title> | 9 | <title>LPHUB | Homepage</title> |
| 10 | </Helmet> | 10 | </Helmet> |
| 11 | <section> | 11 | <section> |
| 12 | <p /> | 12 | <p /> |
| 13 | <h1>Welcome to Least Portals Hub!</h1> | 13 | <h1>Welcome to Least Portals Hub!</h1> |
| 14 | <p>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.</p> | 14 | <p>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.</p> |
| 15 | <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p> | 15 | <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p> |
| 16 | <p>By using LPHUB, you agree that you have read the 'Leaderboard Rules' and the 'About LPHUB' pages.</p> | 16 | <p>By using LPHUB, you agree that you have read the 'Leaderboard Rules' and the 'About LPHUB' pages.</p> |
| 17 | </section> | 17 | </section> |
| 18 | </main> | 18 | </main> |
| 19 | ); | 19 | ); |
| 20 | }; | 20 | }; |
| 21 | 21 | ||
| 22 | export default Homepage; | 22 | export default Homepage; |
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx index fb13563..8bb5b32 100644 --- a/frontend/src/pages/Maps.tsx +++ b/frontend/src/pages/Maps.tsx | |||
| @@ -1,14 +1,14 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link, useLocation } from 'react-router-dom'; | 2 | import { Link, useLocation } from "react-router-dom"; |
| 3 | import { Helmet } from 'react-helmet'; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import { PortalIcon, FlagIcon, ChatIcon } from '@images/Images'; | 5 | import { PortalIcon, FlagIcon, ChatIcon } from "@images/Images"; |
| 6 | import Summary from '@components/Summary'; | 6 | import Summary from "@components/Summary"; |
| 7 | import Leaderboards from '@components/Leaderboards'; | 7 | import Leaderboards from "@components/Leaderboards"; |
| 8 | import Discussions from '@components/Discussions'; | 8 | import Discussions from "@components/Discussions"; |
| 9 | import ModMenu from '@components/ModMenu'; | 9 | import ModMenu from "@components/ModMenu"; |
| 10 | import { MapDiscussions, MapLeaderboard, MapSummary } from '@customTypes/Map'; | 10 | import { MapDiscussions, MapLeaderboard, MapSummary } from "@customTypes/Map"; |
| 11 | import { API } from '@api/Api'; | 11 | import { API } from "@api/Api"; |
| 12 | import "@css/Maps.css"; | 12 | import "@css/Maps.css"; |
| 13 | 13 | ||
| 14 | interface MapProps { | 14 | interface MapProps { |
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 48233bf..e7b8325 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx | |||
| @@ -1,16 +1,16 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link, useNavigate } from 'react-router-dom'; | 2 | import { Link, useNavigate } from "react-router-dom"; |
| 3 | import { Helmet } from 'react-helmet'; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon, DeleteIcon } from '@images/Images'; | 5 | import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon, DeleteIcon } from "@images/Images"; |
| 6 | import { UserProfile } from '@customTypes/Profile'; | 6 | import { UserProfile } from "@customTypes/Profile"; |
| 7 | import { Game, GameChapters } from '@customTypes/Game'; | 7 | import { Game, GameChapters } from "@customTypes/Game"; |
| 8 | import { Map } from '@customTypes/Map'; | 8 | import { Map } from "@customTypes/Map"; |
| 9 | import { ticks_to_time } from '@utils/Time'; | 9 | import { ticks_to_time } from "@utils/Time"; |
| 10 | import "@css/Profile.css"; | 10 | import "@css/Profile.css"; |
| 11 | import { API } from '@api/Api'; | 11 | import { API } from "@api/Api"; |
| 12 | import useConfirm from '@hooks/UseConfirm'; | 12 | import useConfirm from "@hooks/UseConfirm"; |
| 13 | import useMessage from '@hooks/UseMessage'; | 13 | import useMessage from "@hooks/UseMessage"; |
| 14 | import useMessageLoad from "@hooks/UseMessageLoad"; | 14 | import useMessageLoad from "@hooks/UseMessageLoad"; |
| 15 | 15 | ||
| 16 | interface ProfileProps { | 16 | interface ProfileProps { |
| @@ -193,9 +193,9 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 193 | 193 | ||
| 194 | <select id='select-game' | 194 | <select id='select-game' |
| 195 | onChange={() => { | 195 | onChange={() => { |
| 196 | setGame((document.querySelector('#select-game') as HTMLInputElement).value); | 196 | setGame((document.querySelector("#select-game") as HTMLInputElement).value); |
| 197 | setChapter("0"); | 197 | setChapter("0"); |
| 198 | const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; | 198 | const chapterSelect = document.querySelector("#select-chapter") as HTMLSelectElement; |
| 199 | if (chapterSelect) { | 199 | if (chapterSelect) { |
| 200 | chapterSelect.value = "0"; | 200 | chapterSelect.value = "0"; |
| 201 | } | 201 | } |
| @@ -213,7 +213,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 213 | : chapterData === null ? <select></select> : | 213 | : chapterData === null ? <select></select> : |
| 214 | 214 | ||
| 215 | <select id='select-chapter' | 215 | <select id='select-chapter' |
| 216 | onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> | 216 | onChange={() => setChapter((document.querySelector("#select-chapter") as HTMLInputElement).value)}> |
| 217 | <option value="0" key="0">All Chapters</option> | 217 | <option value="0" key="0">All Chapters</option> |
| 218 | {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( | 218 | {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( |
| 219 | <option value={e.id} key={i + 1}>{e.name}</option> | 219 | <option value={e.id} key={i + 1}>{e.name}</option> |
| @@ -222,9 +222,9 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 222 | </div> | 222 | </div> |
| 223 | <div id='profileboard-top'> | 223 | <div id='profileboard-top'> |
| 224 | <span><span>Map Name</span><img src={SortIcon} alt="" /></span> | 224 | <span><span>Map Name</span><img src={SortIcon} alt="" /></span> |
| 225 | <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> | 225 | <span style={{ justifyContent: "center" }}><span>Portals</span><img src={SortIcon} alt="" /></span> |
| 226 | <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> | 226 | <span style={{ justifyContent: "center" }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> |
| 227 | <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> | 227 | <span style={{ justifyContent: "center" }}><span>Time</span><img src={SortIcon} alt="" /></span> |
| 228 | <span> </span> | 228 | <span> </span> |
| 229 | <span><span>Rank</span><img src={SortIcon} alt="" /></span> | 229 | <span><span>Rank</span><img src={SortIcon} alt="" /></span> |
| 230 | <span><span>Date</span><img src={SortIcon} alt="" /></span> | 230 | <span><span>Date</span><img src={SortIcon} alt="" /></span> |
| @@ -239,7 +239,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 239 | }); | 239 | }); |
| 240 | } | 240 | } |
| 241 | }} | 241 | }} |
| 242 | ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> | 242 | ><i className='triangle' style={{ position: "relative", left: "-5px", }}></i> </button> |
| 243 | <span>{pageNumber}/{pageMax}</span> | 243 | <span>{pageNumber}/{pageMax}</span> |
| 244 | <button onClick={() => { | 244 | <button onClick={() => { |
| 245 | if (pageNumber !== pageMax) { | 245 | if (pageNumber !== pageMax) { |
| @@ -250,7 +250,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 250 | }); | 250 | }); |
| 251 | } | 251 | } |
| 252 | }} | 252 | }} |
| 253 | ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> | 253 | ><i className='triangle' style={{ position: "relative", left: "5px", transform: "rotate(180deg)" }}></i> </button> |
| 254 | </div> | 254 | </div> |
| 255 | </div> | 255 | </div> |
| 256 | </div> | 256 | </div> |
| @@ -272,7 +272,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 272 | 272 | ||
| 273 | <span style={{ display: "grid" }}>{e.score_count}</span> | 273 | <span style={{ display: "grid" }}>{e.score_count}</span> |
| 274 | 274 | ||
| 275 | <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> | 275 | <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : "-"}</span> |
| 276 | <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> | 276 | <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> |
| 277 | <span> </span> | 277 | <span> </span> |
| 278 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} | 278 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} |
| @@ -300,7 +300,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 300 | maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) | 300 | maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) |
| 301 | .map((r, index) => { | 301 | .map((r, index) => { |
| 302 | if (Math.ceil((index + 1) / 20) === pageNumber) { | 302 | if (Math.ceil((index + 1) / 20) === pageNumber) { |
| 303 | let record = profile.records.find((e) => e.map_id === r.id); | 303 | const record = profile.records.find((e) => e.map_id === r.id); |
| 304 | return record === undefined ? ( | 304 | return record === undefined ? ( |
| 305 | <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> | 305 | <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> |
| 306 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 306 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> |
| @@ -318,7 +318,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 318 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | 318 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} |
| 319 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 319 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> |
| 320 | <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> | 320 | <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> |
| 321 | <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</span> | 321 | <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : "-"}</span> |
| 322 | <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> | 322 | <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> |
| 323 | <span> </span> | 323 | <span> </span> |
| 324 | {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} | 324 | {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} |
diff --git a/frontend/src/pages/Rankings.tsx b/frontend/src/pages/Rankings.tsx index 71aa427..12dcab4 100644 --- a/frontend/src/pages/Rankings.tsx +++ b/frontend/src/pages/Rankings.tsx | |||
| @@ -8,140 +8,140 @@ import { API } from "@api/Api"; | |||
| 8 | import "@css/Rankings.css"; | 8 | import "@css/Rankings.css"; |
| 9 | 9 | ||
| 10 | const Rankings: React.FC = () => { | 10 | const Rankings: React.FC = () => { |
| 11 | const [leaderboardData, setLeaderboardData] = React.useState<Ranking | SteamRanking>(); | 11 | const [leaderboardData, setLeaderboardData] = React.useState<Ranking | SteamRanking>(); |
| 12 | const [currentLeaderboard, setCurrentLeaderboard] = React.useState<RankingType[] | SteamRankingType[]>(); | 12 | const [currentLeaderboard, setCurrentLeaderboard] = React.useState<RankingType[] | SteamRankingType[]>(); |
| 13 | enum LeaderboardTypes { | 13 | enum LeaderboardTypes { |
| 14 | official, | 14 | official, |
| 15 | unofficial | 15 | unofficial |
| 16 | } | ||
| 17 | const [currentRankingType, setCurrentRankingType] = React.useState<LeaderboardTypes>(LeaderboardTypes.official); | ||
| 18 | |||
| 19 | const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false); | ||
| 20 | |||
| 21 | enum RankingCategories { | ||
| 22 | rankings_overall, | ||
| 23 | rankings_multiplayer, | ||
| 24 | rankings_singleplayer | ||
| 25 | } | ||
| 26 | const [currentLeaderboardType, setCurrentLeaderboardType] = React.useState<RankingCategories>(RankingCategories.rankings_singleplayer); | ||
| 27 | const [load, setLoad] = React.useState<boolean>(false); | ||
| 28 | |||
| 29 | const _fetch_rankings = async () => { | ||
| 30 | setLeaderboardLoad(false); | ||
| 31 | const rankings = await API.get_official_rankings(); | ||
| 32 | setLeaderboardData(rankings); | ||
| 33 | if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { | ||
| 34 | setCurrentLeaderboard(rankings.rankings_singleplayer) | ||
| 35 | } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { | ||
| 36 | setCurrentLeaderboard(rankings.rankings_multiplayer) | ||
| 37 | } else { | ||
| 38 | setCurrentLeaderboard(rankings.rankings_overall) | ||
| 16 | } | 39 | } |
| 17 | const [currentRankingType, setCurrentRankingType] = React.useState<LeaderboardTypes>(LeaderboardTypes.official); | 40 | setLoad(true); |
| 18 | 41 | setLeaderboardLoad(true); | |
| 19 | const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false); | 42 | } |
| 20 | 43 | ||
| 21 | enum RankingCategories { | 44 | const __dev_fetch_unofficial_rankings = async () => { |
| 22 | rankings_overall, | 45 | try { |
| 23 | rankings_multiplayer, | 46 | setLeaderboardLoad(false); |
| 24 | rankings_singleplayer | 47 | const rankings = await API.get_unofficial_rankings(); |
| 48 | setLeaderboardData(rankings); | ||
| 49 | if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { | ||
| 50 | // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) | ||
| 51 | setCurrentLeaderboard(rankings.rankings_singleplayer) | ||
| 52 | } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { | ||
| 53 | setCurrentLeaderboard(rankings.rankings_multiplayer) | ||
| 54 | } else { | ||
| 55 | setCurrentLeaderboard(rankings.rankings_overall) | ||
| 56 | } | ||
| 57 | setLeaderboardLoad(true); | ||
| 58 | } catch (e) { | ||
| 59 | console.log(e) | ||
| 25 | } | 60 | } |
| 26 | const [currentLeaderboardType, setCurrentLeaderboardType] = React.useState<RankingCategories>(RankingCategories.rankings_singleplayer); | 61 | } |
| 27 | const [load, setLoad] = React.useState<boolean>(false); | 62 | |
| 28 | 63 | const _set_current_leaderboard = (ranking_cat: RankingCategories) => { | |
| 29 | const _fetch_rankings = async () => { | 64 | if (ranking_cat == RankingCategories.rankings_singleplayer) { |
| 30 | setLeaderboardLoad(false); | 65 | setCurrentLeaderboard(leaderboardData!.rankings_singleplayer); |
| 31 | const rankings = await API.get_official_rankings(); | 66 | } else if (ranking_cat == RankingCategories.rankings_multiplayer) { |
| 32 | setLeaderboardData(rankings); | 67 | setCurrentLeaderboard(leaderboardData!.rankings_multiplayer); |
| 33 | if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { | 68 | } else { |
| 34 | setCurrentLeaderboard(rankings.rankings_singleplayer) | 69 | setCurrentLeaderboard(leaderboardData!.rankings_overall); |
| 35 | } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { | ||
| 36 | setCurrentLeaderboard(rankings.rankings_multiplayer) | ||
| 37 | } else { | ||
| 38 | setCurrentLeaderboard(rankings.rankings_overall) | ||
| 39 | } | ||
| 40 | setLoad(true); | ||
| 41 | setLeaderboardLoad(true); | ||
| 42 | } | 70 | } |
| 43 | 71 | ||
| 44 | const __dev_fetch_unofficial_rankings = async () => { | 72 | setCurrentLeaderboardType(ranking_cat); |
| 45 | try { | 73 | } |
| 46 | setLeaderboardLoad(false); | ||
| 47 | const rankings = await API.get_unofficial_rankings(); | ||
| 48 | setLeaderboardData(rankings); | ||
| 49 | if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { | ||
| 50 | // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) | ||
| 51 | setCurrentLeaderboard(rankings.rankings_singleplayer) | ||
| 52 | } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { | ||
| 53 | setCurrentLeaderboard(rankings.rankings_multiplayer) | ||
| 54 | } else { | ||
| 55 | setCurrentLeaderboard(rankings.rankings_overall) | ||
| 56 | } | ||
| 57 | setLeaderboardLoad(true); | ||
| 58 | } catch (e) { | ||
| 59 | console.log(e) | ||
| 60 | } | ||
| 61 | } | ||
| 62 | 74 | ||
| 63 | const _set_current_leaderboard = (ranking_cat: RankingCategories) => { | 75 | const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => { |
| 64 | if (ranking_cat == RankingCategories.rankings_singleplayer) { | 76 | if (leaderboard_type == LeaderboardTypes.official) { |
| 65 | setCurrentLeaderboard(leaderboardData!.rankings_singleplayer); | 77 | _fetch_rankings(); |
| 66 | } else if (ranking_cat == RankingCategories.rankings_multiplayer) { | 78 | } else { |
| 67 | setCurrentLeaderboard(leaderboardData!.rankings_multiplayer); | ||
| 68 | } else { | ||
| 69 | setCurrentLeaderboard(leaderboardData!.rankings_overall); | ||
| 70 | } | ||
| 71 | 79 | ||
| 72 | setCurrentLeaderboardType(ranking_cat); | ||
| 73 | } | 80 | } |
| 81 | } | ||
| 74 | 82 | ||
| 75 | const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => { | 83 | useEffect(() => { |
| 76 | if (leaderboard_type == LeaderboardTypes.official) { | 84 | _fetch_rankings(); |
| 77 | _fetch_rankings(); | 85 | if (load) { |
| 78 | } else { | 86 | _set_current_leaderboard(RankingCategories.rankings_singleplayer); |
| 79 | |||
| 80 | } | ||
| 81 | } | 87 | } |
| 88 | }, [load]) | ||
| 89 | |||
| 90 | return ( | ||
| 91 | <main> | ||
| 92 | <Helmet> | ||
| 93 | <title>LPHUB | Rankings</title> | ||
| 94 | </Helmet> | ||
| 95 | <section className="nav-container nav-1"> | ||
| 96 | <div> | ||
| 97 | <button onClick={() => { _fetch_rankings(); setCurrentRankingType(LeaderboardTypes.official) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? "selected" : ""}`}> | ||
| 98 | <span>Official (LPHUB)</span> | ||
| 99 | </button> | ||
| 100 | <button onClick={() => { __dev_fetch_unofficial_rankings(); setCurrentRankingType(LeaderboardTypes.unofficial) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? "selected" : ""}`}> | ||
| 101 | <span>Unofficial (Steam)</span> | ||
| 102 | </button> | ||
| 103 | </div> | ||
| 104 | </section> | ||
| 105 | <section className="nav-container nav-2"> | ||
| 106 | <div> | ||
| 107 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_singleplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_singleplayer ? "selected" : ""}`}> | ||
| 108 | <span>Singleplayer</span> | ||
| 109 | </button> | ||
| 110 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_multiplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_multiplayer ? "selected" : ""}`}> | ||
| 111 | <span>Cooperative</span> | ||
| 112 | </button> | ||
| 113 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_overall)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_overall ? "selected" : ""}`}> | ||
| 114 | <span>Overall</span> | ||
| 115 | </button> | ||
| 116 | </div> | ||
| 117 | </section> | ||
| 118 | |||
| 119 | {load ? | ||
| 120 | <section className="rankings-leaderboard"> | ||
| 121 | <div className="ranks-container"> | ||
| 122 | <div className="leaderboard-entry header"> | ||
| 123 | <span>Rank</span> | ||
| 124 | <span>Player</span> | ||
| 125 | <span>Portals</span> | ||
| 126 | </div> | ||
| 127 | |||
| 128 | <div className="splitter"></div> | ||
| 129 | |||
| 130 | {leaderboardLoad && currentLeaderboard?.map((curRankingData, i) => { | ||
| 131 | return <RankingEntry currentLeaderboardType={currentLeaderboardType} curRankingData={curRankingData} key={i}></RankingEntry> | ||
| 132 | }) | ||
| 133 | } | ||
| 82 | 134 | ||
| 83 | useEffect(() => { | 135 | {leaderboardLoad ? null : |
| 84 | _fetch_rankings(); | 136 | <div style={{ display: "flex", justifyContent: "center", margin: "30px 0px" }}> |
| 85 | if (load) { | 137 | <span className="loader"></span> |
| 86 | _set_current_leaderboard(RankingCategories.rankings_singleplayer); | 138 | </div> |
| 87 | } | 139 | } |
| 88 | }, [load]) | 140 | </div> |
| 89 | 141 | </section> | |
| 90 | return ( | 142 | : null} |
| 91 | <main> | 143 | </main> |
| 92 | <Helmet> | 144 | ) |
| 93 | <title>LPHUB | Rankings</title> | ||
| 94 | </Helmet> | ||
| 95 | <section className="nav-container nav-1"> | ||
| 96 | <div> | ||
| 97 | <button onClick={() => { _fetch_rankings(); setCurrentRankingType(LeaderboardTypes.official) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? "selected" : ""}`}> | ||
| 98 | <span>Official (LPHUB)</span> | ||
| 99 | </button> | ||
| 100 | <button onClick={() => { __dev_fetch_unofficial_rankings(); setCurrentRankingType(LeaderboardTypes.unofficial) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? "selected" : ""}`}> | ||
| 101 | <span>Unofficial (Steam)</span> | ||
| 102 | </button> | ||
| 103 | </div> | ||
| 104 | </section> | ||
| 105 | <section className="nav-container nav-2"> | ||
| 106 | <div> | ||
| 107 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_singleplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_singleplayer ? "selected" : ""}`}> | ||
| 108 | <span>Singleplayer</span> | ||
| 109 | </button> | ||
| 110 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_multiplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_multiplayer ? "selected" : ""}`}> | ||
| 111 | <span>Cooperative</span> | ||
| 112 | </button> | ||
| 113 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_overall)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_overall ? "selected" : ""}`}> | ||
| 114 | <span>Overall</span> | ||
| 115 | </button> | ||
| 116 | </div> | ||
| 117 | </section> | ||
| 118 | |||
| 119 | {load ? | ||
| 120 | <section className="rankings-leaderboard"> | ||
| 121 | <div className="ranks-container"> | ||
| 122 | <div className="leaderboard-entry header"> | ||
| 123 | <span>Rank</span> | ||
| 124 | <span>Player</span> | ||
| 125 | <span>Portals</span> | ||
| 126 | </div> | ||
| 127 | |||
| 128 | <div className="splitter"></div> | ||
| 129 | |||
| 130 | {leaderboardLoad && currentLeaderboard?.map((curRankingData, i) => { | ||
| 131 | return <RankingEntry currentLeaderboardType={currentLeaderboardType} curRankingData={curRankingData} key={i}></RankingEntry> | ||
| 132 | }) | ||
| 133 | } | ||
| 134 | |||
| 135 | {leaderboardLoad ? null : | ||
| 136 | <div style={{ display: "flex", justifyContent: "center", margin: "30px 0px" }}> | ||
| 137 | <span className="loader"></span> | ||
| 138 | </div> | ||
| 139 | } | ||
| 140 | </div> | ||
| 141 | </section> | ||
| 142 | : null} | ||
| 143 | </main> | ||
| 144 | ) | ||
| 145 | } | 145 | } |
| 146 | 146 | ||
| 147 | export default Rankings; | 147 | export default Rankings; |
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx index 9f57b7e..a07c0c1 100644 --- a/frontend/src/pages/Rules.tsx +++ b/frontend/src/pages/Rules.tsx | |||
| @@ -1,41 +1,41 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import ReactMarkdown from 'react-markdown'; | 2 | import ReactMarkdown from "react-markdown"; |
| 3 | import { Helmet } from 'react-helmet'; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import '@css/Rules.css'; | 5 | import "@css/Rules.css"; |
| 6 | 6 | ||
| 7 | const Rules: React.FC = () => { | 7 | const Rules: React.FC = () => { |
| 8 | 8 | ||
| 9 | const [rulesText, setRulesText] = React.useState<string>(""); | 9 | const [rulesText, setRulesText] = React.useState<string>(""); |
| 10 | 10 | ||
| 11 | React.useEffect(() => { | 11 | React.useEffect(() => { |
| 12 | const fetchRules = async () => { | 12 | const fetchRules = async () => { |
| 13 | try { | 13 | try { |
| 14 | const response = await fetch( | 14 | const response = await fetch( |
| 15 | 'https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md' | 15 | "https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md" |
| 16 | ); | 16 | ); |
| 17 | if (!response.ok) { | 17 | if (!response.ok) { |
| 18 | throw new Error('Failed to fetch README'); | 18 | throw new Error("Failed to fetch README"); |
| 19 | } | 19 | } |
| 20 | const rulesText = await response.text(); | 20 | const rulesText = await response.text(); |
| 21 | setRulesText(rulesText); | 21 | setRulesText(rulesText); |
| 22 | } catch (error) { | 22 | } catch (error) { |
| 23 | console.error('Error fetching Rules:', error); | 23 | console.error("Error fetching Rules:", error); |
| 24 | } | 24 | } |
| 25 | // setRulesText(rulesText) | 25 | // setRulesText(rulesText) |
| 26 | }; | 26 | }; |
| 27 | fetchRules(); | 27 | fetchRules(); |
| 28 | }, []); | 28 | }, []); |
| 29 | 29 | ||
| 30 | 30 | ||
| 31 | return ( | 31 | return ( |
| 32 | <main> | 32 | <main> |
| 33 | <Helmet> | 33 | <Helmet> |
| 34 | <title>LPHUB | Rules</title> | 34 | <title>LPHUB | Rules</title> |
| 35 | </Helmet> | 35 | </Helmet> |
| 36 | <ReactMarkdown>{rulesText}</ReactMarkdown> | 36 | <ReactMarkdown>{rulesText}</ReactMarkdown> |
| 37 | </main> | 37 | </main> |
| 38 | ); | 38 | ); |
| 39 | }; | 39 | }; |
| 40 | 40 | ||
| 41 | export default Rules; | 41 | export default Rules; |
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx index d43c0c6..33be1f0 100644 --- a/frontend/src/pages/User.tsx +++ b/frontend/src/pages/User.tsx | |||
| @@ -1,15 +1,15 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link, useLocation, useNavigate } from 'react-router-dom'; | 2 | import { Link, useLocation, useNavigate } from "react-router-dom"; |
| 3 | import { Helmet } from 'react-helmet'; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '@images/Images'; | 5 | import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from "@images/Images"; |
| 6 | import { UserProfile } from '@customTypes/Profile'; | 6 | import { UserProfile } from "@customTypes/Profile"; |
| 7 | import { Game, GameChapters } from '@customTypes/Game'; | 7 | import { Game, GameChapters } from "@customTypes/Game"; |
| 8 | import { Map } from '@customTypes/Map'; | 8 | import { Map } from "@customTypes/Map"; |
| 9 | import { API } from '@api/Api'; | 9 | import { API } from "@api/Api"; |
| 10 | import { ticks_to_time } from '@utils/Time'; | 10 | import { ticks_to_time } from "@utils/Time"; |
| 11 | import "@css/Profile.css"; | 11 | import "@css/Profile.css"; |
| 12 | import useMessage from '@hooks/UseMessage'; | 12 | import useMessage from "@hooks/UseMessage"; |
| 13 | 13 | ||
| 14 | interface UserProps { | 14 | interface UserProps { |
| 15 | profile?: UserProfile; | 15 | profile?: UserProfile; |
| @@ -162,9 +162,9 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 162 | 162 | ||
| 163 | <select id='select-game' | 163 | <select id='select-game' |
| 164 | onChange={() => { | 164 | onChange={() => { |
| 165 | setGame((document.querySelector('#select-game') as HTMLInputElement).value); | 165 | setGame((document.querySelector("#select-game") as HTMLInputElement).value); |
| 166 | setChapter("0"); | 166 | setChapter("0"); |
| 167 | const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; | 167 | const chapterSelect = document.querySelector("#select-chapter") as HTMLSelectElement; |
| 168 | if (chapterSelect) { | 168 | if (chapterSelect) { |
| 169 | chapterSelect.value = "0"; | 169 | chapterSelect.value = "0"; |
| 170 | } | 170 | } |
| @@ -182,7 +182,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 182 | : chapterData === null ? <select></select> : | 182 | : chapterData === null ? <select></select> : |
| 183 | 183 | ||
| 184 | <select id='select-chapter' | 184 | <select id='select-chapter' |
| 185 | onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> | 185 | onChange={() => setChapter((document.querySelector("#select-chapter") as HTMLInputElement).value)}> |
| 186 | <option value="0" key="0">All Chapters</option> | 186 | <option value="0" key="0">All Chapters</option> |
| 187 | {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( | 187 | {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( |
| 188 | <option value={e.id} key={i + 1}>{e.name}</option> | 188 | <option value={e.id} key={i + 1}>{e.name}</option> |
| @@ -191,9 +191,9 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 191 | </div> | 191 | </div> |
| 192 | <div id='profileboard-top'> | 192 | <div id='profileboard-top'> |
| 193 | <span><span>Map Name</span><img src={SortIcon} alt="" /></span> | 193 | <span><span>Map Name</span><img src={SortIcon} alt="" /></span> |
| 194 | <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> | 194 | <span style={{ justifyContent: "center" }}><span>Portals</span><img src={SortIcon} alt="" /></span> |
| 195 | <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> | 195 | <span style={{ justifyContent: "center" }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> |
| 196 | <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> | 196 | <span style={{ justifyContent: "center" }}><span>Time</span><img src={SortIcon} alt="" /></span> |
| 197 | <span> </span> | 197 | <span> </span> |
| 198 | <span><span>Rank</span><img src={SortIcon} alt="" /></span> | 198 | <span><span>Rank</span><img src={SortIcon} alt="" /></span> |
| 199 | <span><span>Date</span><img src={SortIcon} alt="" /></span> | 199 | <span><span>Date</span><img src={SortIcon} alt="" /></span> |
| @@ -208,7 +208,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 208 | }); | 208 | }); |
| 209 | } | 209 | } |
| 210 | }} | 210 | }} |
| 211 | ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> | 211 | ><i className='triangle' style={{ position: "relative", left: "-5px", }}></i> </button> |
| 212 | <span>{pageNumber}/{pageMax}</span> | 212 | <span>{pageNumber}/{pageMax}</span> |
| 213 | <button onClick={() => { | 213 | <button onClick={() => { |
| 214 | if (pageNumber !== pageMax) { | 214 | if (pageNumber !== pageMax) { |
| @@ -219,7 +219,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 219 | }); | 219 | }); |
| 220 | } | 220 | } |
| 221 | }} | 221 | }} |
| 222 | ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> | 222 | ><i className='triangle' style={{ position: "relative", left: "5px", transform: "rotate(180deg)" }}></i> </button> |
| 223 | </div> | 223 | </div> |
| 224 | </div> | 224 | </div> |
| 225 | </div> | 225 | </div> |
| @@ -241,7 +241,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 241 | 241 | ||
| 242 | <span style={{ display: "grid" }}>{e.score_count}</span> | 242 | <span style={{ display: "grid" }}>{e.score_count}</span> |
| 243 | 243 | ||
| 244 | <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> | 244 | <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : "-"}</span> |
| 245 | <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> | 245 | <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> |
| 246 | <span> </span> | 246 | <span> </span> |
| 247 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} | 247 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} |
| @@ -268,7 +268,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 268 | maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) | 268 | maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) |
| 269 | .map((r, index) => { | 269 | .map((r, index) => { |
| 270 | if (Math.ceil((index + 1) / 20) === pageNumber) { | 270 | if (Math.ceil((index + 1) / 20) === pageNumber) { |
| 271 | let record = user.records.find((e) => e.map_id === r.id); | 271 | const record = user.records.find((e) => e.map_id === r.id); |
| 272 | return record === undefined ? ( | 272 | return record === undefined ? ( |
| 273 | <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> | 273 | <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> |
| 274 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 274 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> |
| @@ -286,7 +286,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 286 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | 286 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} |
| 287 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 287 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> |
| 288 | <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> | 288 | <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> |
| 289 | <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</span> | 289 | <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : "-"}</span> |
| 290 | <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> | 290 | <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> |
| 291 | <span> </span> | 291 | <span> </span> |
| 292 | {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} | 292 | {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} |
diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts deleted file mode 100644 index 8265915..0000000 --- a/frontend/src/react-app-env.d.ts +++ /dev/null | |||
| @@ -1,2 +0,0 @@ | |||
| 1 | declare module "*.png"; | ||
| 2 | declare module "*.css"; | ||
diff --git a/frontend/src/types/Content.ts b/frontend/src/types/Content.ts index 775fab4..accb441 100644 --- a/frontend/src/types/Content.ts +++ b/frontend/src/types/Content.ts | |||
| @@ -13,10 +13,6 @@ export interface MapDiscussionContent { | |||
| 13 | content: string; | 13 | content: string; |
| 14 | }; | 14 | }; |
| 15 | 15 | ||
| 16 | export interface MapDiscussionCommentContent { | ||
| 17 | comment: string; | ||
| 18 | }; | ||
| 19 | |||
| 20 | export interface UploadRunContent { | 16 | export interface UploadRunContent { |
| 21 | host_demo: File | null; | 17 | host_demo: File | null; |
| 22 | partner_demo: File | null; | 18 | partner_demo: File | null; |
diff --git a/frontend/src/types/Game.ts b/frontend/src/types/Game.ts index 1a80341..104d02a 100644 --- a/frontend/src/types/Game.ts +++ b/frontend/src/types/Game.ts | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | import type { Map } from '@customTypes/Map'; | 1 | import type { Map } from "@customTypes/Map"; |
| 2 | 2 | ||
| 3 | 3 | ||
| 4 | export interface Game { | 4 | export interface Game { |
diff --git a/frontend/src/types/Map.ts b/frontend/src/types/Map.ts index 4f8eabf..b12d965 100644 --- a/frontend/src/types/Map.ts +++ b/frontend/src/types/Map.ts | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | import type { Category, GameCategoryPortals } from '@customTypes/Game'; | 1 | import type { Category, GameCategoryPortals } from "@customTypes/Game"; |
| 2 | import type { Pagination } from '@customTypes/Pagination'; | 2 | import type { Pagination } from "@customTypes/Pagination"; |
| 3 | import type { UserShort } from '@customTypes/Profile'; | 3 | import type { UserShort } from "@customTypes/Profile"; |
| 4 | 4 | ||
| 5 | export interface Map { | 5 | export interface Map { |
| 6 | id: number; | 6 | id: number; |
diff --git a/frontend/src/types/MapNames.ts b/frontend/src/types/MapNames.ts index b6313e7..7a408a3 100644 --- a/frontend/src/types/MapNames.ts +++ b/frontend/src/types/MapNames.ts | |||
| @@ -1,127 +1,127 @@ | |||
| 1 | export const MapNames: { [key: string]: number } = { | 1 | export const MapNames: { [key: string]: number } = { |
| 2 | "sp_a1_intro1": 1, | 2 | "sp_a1_intro1": 1, |
| 3 | "sp_a1_intro2": 2, | 3 | "sp_a1_intro2": 2, |
| 4 | "sp_a1_intro3": 3, | 4 | "sp_a1_intro3": 3, |
| 5 | "sp_a1_intro4": 4, | 5 | "sp_a1_intro4": 4, |
| 6 | "sp_a1_intro5": 5, | 6 | "sp_a1_intro5": 5, |
| 7 | "sp_a1_intro6": 6, | 7 | "sp_a1_intro6": 6, |
| 8 | "sp_a1_intro7": 7, | 8 | "sp_a1_intro7": 7, |
| 9 | "sp_a1_wakeup": 8, | 9 | "sp_a1_wakeup": 8, |
| 10 | "sp_a2_intro": 9, | 10 | "sp_a2_intro": 9, |
| 11 | 11 | ||
| 12 | "sp_a2_laser_intro": 10, | 12 | "sp_a2_laser_intro": 10, |
| 13 | "sp_a2_laser_stairs": 11, | 13 | "sp_a2_laser_stairs": 11, |
| 14 | "sp_a2_dual_lasers": 12, | 14 | "sp_a2_dual_lasers": 12, |
| 15 | "sp_a2_laser_over_goo": 13, | 15 | "sp_a2_laser_over_goo": 13, |
| 16 | "sp_a2_catapult_intro": 14, | 16 | "sp_a2_catapult_intro": 14, |
| 17 | "sp_a2_trust_fling": 15, | 17 | "sp_a2_trust_fling": 15, |
| 18 | "sp_a2_pit_flings": 16, | 18 | "sp_a2_pit_flings": 16, |
| 19 | "sp_a2_fizzler_intro": 17, | 19 | "sp_a2_fizzler_intro": 17, |
| 20 | 20 | ||
| 21 | "sp_a2_sphere_peek": 18, | 21 | "sp_a2_sphere_peek": 18, |
| 22 | "sp_a2_ricochet": 19, | 22 | "sp_a2_ricochet": 19, |
| 23 | "sp_a2_bridge_intro": 20, | 23 | "sp_a2_bridge_intro": 20, |
| 24 | "sp_a2_bridge_the_gap": 21, | 24 | "sp_a2_bridge_the_gap": 21, |
| 25 | "sp_a2_turret_intro": 22, | 25 | "sp_a2_turret_intro": 22, |
| 26 | "sp_a2_laser_relays": 23, | 26 | "sp_a2_laser_relays": 23, |
| 27 | "sp_a2_turret_blocker": 24, | 27 | "sp_a2_turret_blocker": 24, |
| 28 | "sp_a2_laser_vs_turret": 25, | 28 | "sp_a2_laser_vs_turret": 25, |
| 29 | "sp_a2_pull_the_rug": 26, | 29 | "sp_a2_pull_the_rug": 26, |
| 30 | 30 | ||
| 31 | "sp_a2_column_blocker": 27, | 31 | "sp_a2_column_blocker": 27, |
| 32 | "sp_a2_laser_chaining": 28, | 32 | "sp_a2_laser_chaining": 28, |
| 33 | "sp_a2_triple_laser": 29, | 33 | "sp_a2_triple_laser": 29, |
| 34 | "sp_a2_bts1": 30, | 34 | "sp_a2_bts1": 30, |
| 35 | "sp_a2_bts2": 31, | 35 | "sp_a2_bts2": 31, |
| 36 | 36 | ||
| 37 | "sp_a2_bts3": 32, | 37 | "sp_a2_bts3": 32, |
| 38 | "sp_a2_bts4": 33, | 38 | "sp_a2_bts4": 33, |
| 39 | "sp_a2_bts5": 34, | 39 | "sp_a2_bts5": 34, |
| 40 | "sp_a2_core": 35, | 40 | "sp_a2_core": 35, |
| 41 | 41 | ||
| 42 | "sp_a3_01": 36, | 42 | "sp_a3_01": 36, |
| 43 | "sp_a3_03": 37, | 43 | "sp_a3_03": 37, |
| 44 | "sp_a3_jump_intro": 38, | 44 | "sp_a3_jump_intro": 38, |
| 45 | "sp_a3_bomb_flings": 39, | 45 | "sp_a3_bomb_flings": 39, |
| 46 | "sp_a3_crazy_box": 40, | 46 | "sp_a3_crazy_box": 40, |
| 47 | "sp_a3_transition01": 41, | 47 | "sp_a3_transition01": 41, |
| 48 | 48 | ||
| 49 | "sp_a3_speed_ramp": 42, | 49 | "sp_a3_speed_ramp": 42, |
| 50 | "sp_a3_speed_flings": 43, | 50 | "sp_a3_speed_flings": 43, |
| 51 | "sp_a3_portal_intro": 44, | 51 | "sp_a3_portal_intro": 44, |
| 52 | "sp_a3_end": 45, | 52 | "sp_a3_end": 45, |
| 53 | 53 | ||
| 54 | "sp_a4_intro": 46, | 54 | "sp_a4_intro": 46, |
| 55 | "sp_a4_tb_intro": 47, | 55 | "sp_a4_tb_intro": 47, |
| 56 | "sp_a4_tb_trust_drop": 48, | 56 | "sp_a4_tb_trust_drop": 48, |
| 57 | "sp_a4_tb_wall_button": 49, | 57 | "sp_a4_tb_wall_button": 49, |
| 58 | "sp_a4_tb_polarity": 50, | 58 | "sp_a4_tb_polarity": 50, |
| 59 | "sp_a4_tb_catch": 51, | 59 | "sp_a4_tb_catch": 51, |
| 60 | "sp_a4_stop_the_box": 52, | 60 | "sp_a4_stop_the_box": 52, |
| 61 | "sp_a4_laser_catapult": 53, | 61 | "sp_a4_laser_catapult": 53, |
| 62 | "sp_a4_laser_platform": 54, | 62 | "sp_a4_laser_platform": 54, |
| 63 | "sp_a4_speed_tb_catch": 55, | 63 | "sp_a4_speed_tb_catch": 55, |
| 64 | "sp_a4_jump_polarity": 56, | 64 | "sp_a4_jump_polarity": 56, |
| 65 | 65 | ||
| 66 | "sp_a4_finale1": 57, | 66 | "sp_a4_finale1": 57, |
| 67 | "sp_a4_finale2": 58, | 67 | "sp_a4_finale2": 58, |
| 68 | "sp_a4_finale3": 59, | 68 | "sp_a4_finale3": 59, |
| 69 | "sp_a4_finale4": 60, | 69 | "sp_a4_finale4": 60, |
| 70 | 70 | ||
| 71 | "mp_coop_start": 61, | 71 | "mp_coop_start": 61, |
| 72 | "mp_coop_lobby_3": 62, | 72 | "mp_coop_lobby_3": 62, |
| 73 | 73 | ||
| 74 | "mp_coop_doors": 63, | 74 | "mp_coop_doors": 63, |
| 75 | "mp_coop_race_2": 64, | 75 | "mp_coop_race_2": 64, |
| 76 | "mp_coop_laser_2": 65, | 76 | "mp_coop_laser_2": 65, |
| 77 | "mp_coop_rat_maze": 66, | 77 | "mp_coop_rat_maze": 66, |
| 78 | "mp_coop_laser_crusher": 67, | 78 | "mp_coop_laser_crusher": 67, |
| 79 | "mp_coop_teambts": 68, | 79 | "mp_coop_teambts": 68, |
| 80 | 80 | ||
| 81 | "mp_coop_fling_3": 69, | 81 | "mp_coop_fling_3": 69, |
| 82 | "mp_coop_infinifling_train": 70, | 82 | "mp_coop_infinifling_train": 70, |
| 83 | "mp_coop_come_along": 71, | 83 | "mp_coop_come_along": 71, |
| 84 | "mp_coop_fling_1": 72, | 84 | "mp_coop_fling_1": 72, |
| 85 | "mp_coop_catapult_1": 73, | 85 | "mp_coop_catapult_1": 73, |
| 86 | "mp_coop_multifling_1": 74, | 86 | "mp_coop_multifling_1": 74, |
| 87 | "mp_coop_fling_crushers": 75, | 87 | "mp_coop_fling_crushers": 75, |
| 88 | "mp_coop_fan": 76, | 88 | "mp_coop_fan": 76, |
| 89 | 89 | ||
| 90 | "mp_coop_wall_intro": 77, | 90 | "mp_coop_wall_intro": 77, |
| 91 | "mp_coop_wall_2": 78, | 91 | "mp_coop_wall_2": 78, |
| 92 | "mp_coop_catapult_wall_intro": 79, | 92 | "mp_coop_catapult_wall_intro": 79, |
| 93 | "mp_coop_wall_block": 80, | 93 | "mp_coop_wall_block": 80, |
| 94 | "mp_coop_catapult_2": 81, | 94 | "mp_coop_catapult_2": 81, |
| 95 | "mp_coop_turret_walls": 82, | 95 | "mp_coop_turret_walls": 82, |
| 96 | "mp_coop_turret_ball": 83, | 96 | "mp_coop_turret_ball": 83, |
| 97 | "mp_coop_wall_5": 84, | 97 | "mp_coop_wall_5": 84, |
| 98 | 98 | ||
| 99 | "mp_coop_tbeam_redirect": 85, | 99 | "mp_coop_tbeam_redirect": 85, |
| 100 | "mp_coop_tbeam_drill": 86, | 100 | "mp_coop_tbeam_drill": 86, |
| 101 | "mp_coop_tbeam_catch_grind_1": 87, | 101 | "mp_coop_tbeam_catch_grind_1": 87, |
| 102 | "mp_coop_tbeam_laser_1": 88, | 102 | "mp_coop_tbeam_laser_1": 88, |
| 103 | "mp_coop_tbeam_polarity": 89, | 103 | "mp_coop_tbeam_polarity": 89, |
| 104 | "mp_coop_tbeam_polarity2": 90, | 104 | "mp_coop_tbeam_polarity2": 90, |
| 105 | "mp_coop_tbeam_polarity3": 91, | 105 | "mp_coop_tbeam_polarity3": 91, |
| 106 | "mp_coop_tbeam_maze": 92, | 106 | "mp_coop_tbeam_maze": 92, |
| 107 | "mp_coop_tbeam_end": 93, | 107 | "mp_coop_tbeam_end": 93, |
| 108 | 108 | ||
| 109 | "mp_coop_paint_come_along": 94, | 109 | "mp_coop_paint_come_along": 94, |
| 110 | "mp_coop_paint_redirect": 95, | 110 | "mp_coop_paint_redirect": 95, |
| 111 | "mp_coop_paint_bridge": 96, | 111 | "mp_coop_paint_bridge": 96, |
| 112 | "mp_coop_paint_walljumps": 97, | 112 | "mp_coop_paint_walljumps": 97, |
| 113 | "mp_coop_paint_speed_fling": 98, | 113 | "mp_coop_paint_speed_fling": 98, |
| 114 | "mp_coop_paint_red_racer": 99, | 114 | "mp_coop_paint_red_racer": 99, |
| 115 | "mp_coop_paint_speed_catch": 100, | 115 | "mp_coop_paint_speed_catch": 100, |
| 116 | "mp_coop_paint_longjump_intro": 101, | 116 | "mp_coop_paint_longjump_intro": 101, |
| 117 | 117 | ||
| 118 | "mp_coop_separation_1": 102, | 118 | "mp_coop_separation_1": 102, |
| 119 | "mp_coop_tripleaxis": 103, | 119 | "mp_coop_tripleaxis": 103, |
| 120 | "mp_coop_catapult_catch": 104, | 120 | "mp_coop_catapult_catch": 104, |
| 121 | "mp_coop_2paints_1bridge": 105, | 121 | "mp_coop_2paints_1bridge": 105, |
| 122 | "mp_coop_paint_conversion": 106, | 122 | "mp_coop_paint_conversion": 106, |
| 123 | "mp_coop_bridge_catch": 107, | 123 | "mp_coop_bridge_catch": 107, |
| 124 | "mp_coop_laser_tbeam": 108, | 124 | "mp_coop_laser_tbeam": 108, |
| 125 | "mp_coop_paint_rat_maze": 109, | 125 | "mp_coop_paint_rat_maze": 109, |
| 126 | "mp_coop_paint_crazy_box": 110, | 126 | "mp_coop_paint_crazy_box": 110, |
| 127 | }; | 127 | }; |
diff --git a/frontend/src/utils/Jwt.ts b/frontend/src/utils/Jwt.ts index ce351fb..a6d1866 100644 --- a/frontend/src/utils/Jwt.ts +++ b/frontend/src/utils/Jwt.ts | |||
| @@ -3,20 +3,20 @@ export function get_user_id_from_token(token: string | undefined): string | unde | |||
| 3 | if (!token) { | 3 | if (!token) { |
| 4 | return undefined; | 4 | return undefined; |
| 5 | } | 5 | } |
| 6 | const parts = token.split('.'); | 6 | const parts = token.split("."); |
| 7 | if (parts.length !== 3) { | 7 | if (parts.length !== 3) { |
| 8 | return undefined; | 8 | return undefined; |
| 9 | } | 9 | } |
| 10 | const base64Url = parts[1]; | 10 | const base64Url = parts[1]; |
| 11 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | 11 | const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); |
| 12 | 12 | ||
| 13 | const jsonPayload = decodeURIComponent( | 13 | const jsonPayload = decodeURIComponent( |
| 14 | atob(base64) | 14 | atob(base64) |
| 15 | .split('') | 15 | .split("") |
| 16 | .map(function (c) { | 16 | .map(function (c) { |
| 17 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); | 17 | return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); |
| 18 | }) | 18 | }) |
| 19 | .join('') | 19 | .join("") |
| 20 | ); | 20 | ); |
| 21 | return JSON.parse(jsonPayload).sub; | 21 | return JSON.parse(jsonPayload).sub; |
| 22 | }; | 22 | }; |
| @@ -25,20 +25,20 @@ export function get_user_mod_from_token(token: string | undefined): boolean | un | |||
| 25 | if (!token) { | 25 | if (!token) { |
| 26 | return undefined; | 26 | return undefined; |
| 27 | } | 27 | } |
| 28 | const parts = token.split('.'); | 28 | const parts = token.split("."); |
| 29 | if (parts.length !== 3) { | 29 | if (parts.length !== 3) { |
| 30 | return undefined; | 30 | return undefined; |
| 31 | } | 31 | } |
| 32 | const base64Url = parts[1]; | 32 | const base64Url = parts[1]; |
| 33 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | 33 | const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); |
| 34 | 34 | ||
| 35 | const jsonPayload = decodeURIComponent( | 35 | const jsonPayload = decodeURIComponent( |
| 36 | atob(base64) | 36 | atob(base64) |
| 37 | .split('') | 37 | .split("") |
| 38 | .map(function (c) { | 38 | .map(function (c) { |
| 39 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); | 39 | return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); |
| 40 | }) | 40 | }) |
| 41 | .join('') | 41 | .join("") |
| 42 | ); | 42 | ); |
| 43 | return JSON.parse(jsonPayload).mod; | 43 | return JSON.parse(jsonPayload).mod; |
| 44 | }; | 44 | }; |
diff --git a/frontend/src/utils/Time.ts b/frontend/src/utils/Time.ts index b83a7ed..9cbe793 100644 --- a/frontend/src/utils/Time.ts +++ b/frontend/src/utils/Time.ts | |||
| @@ -5,38 +5,38 @@ export function time_ago(date: any) { | |||
| 5 | const seconds = Math.floor((now - localDate.getTime()) / 1000); | 5 | const seconds = Math.floor((now - localDate.getTime()) / 1000); |
| 6 | 6 | ||
| 7 | let interval = Math.floor(seconds / 31536000); | 7 | let interval = Math.floor(seconds / 31536000); |
| 8 | if (interval === 1) {return interval + ' year ago';} | 8 | if (interval === 1) {return interval + " year ago";} |
| 9 | if (interval > 1) {return interval + ' years ago';} | 9 | if (interval > 1) {return interval + " years ago";} |
| 10 | 10 | ||
| 11 | interval = Math.floor(seconds / 2592000); | 11 | interval = Math.floor(seconds / 2592000); |
| 12 | if (interval === 1) {return interval + ' month ago';} | 12 | if (interval === 1) {return interval + " month ago";} |
| 13 | if (interval > 1) {return interval + ' months ago';} | 13 | if (interval > 1) {return interval + " months ago";} |
| 14 | 14 | ||
| 15 | interval = Math.floor(seconds / 86400); | 15 | interval = Math.floor(seconds / 86400); |
| 16 | if (interval === 1) {return interval + ' day ago';} | 16 | if (interval === 1) {return interval + " day ago";} |
| 17 | if (interval > 1) {return interval + ' days ago';} | 17 | if (interval > 1) {return interval + " days ago";} |
| 18 | 18 | ||
| 19 | interval = Math.floor(seconds / 3600); | 19 | interval = Math.floor(seconds / 3600); |
| 20 | if (interval === 1) {return interval + ' hour ago';} | 20 | if (interval === 1) {return interval + " hour ago";} |
| 21 | if (interval > 1) {return interval + ' hours ago';} | 21 | if (interval > 1) {return interval + " hours ago";} |
| 22 | 22 | ||
| 23 | interval = Math.floor(seconds / 60); | 23 | interval = Math.floor(seconds / 60); |
| 24 | if (interval === 1) {return interval + ' minute ago';} | 24 | if (interval === 1) {return interval + " minute ago";} |
| 25 | if (interval > 1) {return interval + ' minutes ago';} | 25 | if (interval > 1) {return interval + " minutes ago";} |
| 26 | 26 | ||
| 27 | if(seconds < 10) return 'just now'; | 27 | if(seconds < 10) return "just now"; |
| 28 | 28 | ||
| 29 | return Math.floor(seconds) + ' seconds ago'; | 29 | return Math.floor(seconds) + " seconds ago"; |
| 30 | }; | 30 | }; |
| 31 | 31 | ||
| 32 | export function ticks_to_time(ticks: number) { | 32 | export function ticks_to_time(ticks: number) { |
| 33 | let seconds = Math.floor(ticks / 60) | 33 | let seconds = Math.floor(ticks / 60) |
| 34 | let minutes = Math.floor(seconds / 60) | 34 | let minutes = Math.floor(seconds / 60) |
| 35 | let hours = Math.floor(minutes / 60) | 35 | const hours = Math.floor(minutes / 60) |
| 36 | 36 | ||
| 37 | let milliseconds = Math.floor((ticks % 60) * 1000 / 60) | 37 | const milliseconds = Math.floor((ticks % 60) * 1000 / 60) |
| 38 | seconds = seconds % 60; | 38 | seconds = seconds % 60; |
| 39 | minutes = minutes % 60; | 39 | minutes = minutes % 60; |
| 40 | 40 | ||
| 41 | return `${hours === 0 ? "" : hours + ":"}${minutes === 0 ? "" : hours > 0 ? minutes.toString().padStart(2, '0') + ":" : (minutes + ":")}${minutes > 0 ? seconds.toString().padStart(2, '0') : seconds}.${milliseconds.toString().padStart(3, '0')}`; | 41 | return `${hours === 0 ? "" : hours + ":"}${minutes === 0 ? "" : hours > 0 ? minutes.toString().padStart(2, "0") + ":" : (minutes + ":")}${minutes > 0 ? seconds.toString().padStart(2, "0") : seconds}.${milliseconds.toString().padStart(3, "0")}`; |
| 42 | }; \ No newline at end of file | 42 | }; \ No newline at end of file |