diff options
| author | FifthWit <fifthwitbusiness@gmail.com> | 2025-01-30 10:44:30 -0600 |
|---|---|---|
| committer | FifthWit <fifthwitbusiness@gmail.com> | 2025-01-30 10:44:30 -0600 |
| commit | e40f07211f5f15dcb138e2520a76d13afd3c0cfd (patch) | |
| tree | 46bad6a17e66d55a4a65088c0b6eb8c48641615a | |
| parent | added prettier for more consistency (diff) | |
| download | lphub-e40f07211f5f15dcb138e2520a76d13afd3c0cfd.tar.gz lphub-e40f07211f5f15dcb138e2520a76d13afd3c0cfd.tar.bz2 lphub-e40f07211f5f15dcb138e2520a76d13afd3c0cfd.zip | |
formatted with prettier
Diffstat (limited to '')
48 files changed, 2936 insertions, 1736 deletions
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 81589f6..754f5a2 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); |
| @@ -78,7 +78,7 @@ const App: React.FC = () => { | |||
| 78 | <UploadRunDialog | 78 | <UploadRunDialog |
| 79 | token={token} | 79 | token={token} |
| 80 | open={uploadRunDialog} | 80 | open={uploadRunDialog} |
| 81 | onClose={(updateProfile) => { | 81 | onClose={updateProfile => { |
| 82 | setUploadRunDialog(false); | 82 | setUploadRunDialog(false); |
| 83 | if (updateProfile) { | 83 | if (updateProfile) { |
| 84 | _set_profile(get_user_id_from_token(token)); | 84 | _set_profile(get_user_id_from_token(token)); |
| @@ -118,7 +118,7 @@ const App: React.FC = () => { | |||
| 118 | <Route path="/rules" element={<Rules />} /> | 118 | <Route path="/rules" element={<Rules />} /> |
| 119 | <Route path="/about" element={<About />} /> | 119 | <Route path="/about" element={<About />} /> |
| 120 | <Route path="/rankings" element={<Rankings />}></Route> | 120 | <Route path="/rankings" element={<Rankings />}></Route> |
| 121 | <Route path="*" element={"404"} /> | 121 | <Route path="*" element={'404'} /> |
| 122 | </Routes> | 122 | </Routes> |
| 123 | </> | 123 | </> |
| 124 | ); | 124 | ); |
diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts index 0e1658c..b98dda3 100644 --- a/frontend/src/api/Api.ts +++ b/frontend/src/api/Api.ts | |||
| @@ -1,14 +1,14 @@ | |||
| 1 | import { 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 { | 4 | import { |
| 5 | get_games, | 5 | get_games, |
| 6 | get_chapters, | 6 | get_chapters, |
| 7 | get_games_chapters, | 7 | get_games_chapters, |
| 8 | get_game_maps, | 8 | get_game_maps, |
| 9 | get_search, | 9 | get_search, |
| 10 | } from "@api/Games"; | 10 | } from '@api/Games'; |
| 11 | import { get_official_rankings, get_unofficial_rankings } from "@api/Rankings"; | 11 | import { get_official_rankings, get_unofficial_rankings } from '@api/Rankings'; |
| 12 | import { | 12 | import { |
| 13 | get_map_summary, | 13 | get_map_summary, |
| 14 | get_map_leaderboard, | 14 | get_map_leaderboard, |
| @@ -19,14 +19,14 @@ import { | |||
| 19 | delete_map_discussion, | 19 | delete_map_discussion, |
| 20 | post_record, | 20 | post_record, |
| 21 | delete_map_record, | 21 | delete_map_record, |
| 22 | } from "@api/Maps"; | 22 | } from '@api/Maps'; |
| 23 | import { | 23 | import { |
| 24 | delete_map_summary, | 24 | delete_map_summary, |
| 25 | post_map_summary, | 25 | post_map_summary, |
| 26 | put_map_image, | 26 | put_map_image, |
| 27 | put_map_summary, | 27 | put_map_summary, |
| 28 | } from "@api/Mod"; | 28 | } from '@api/Mod'; |
| 29 | import { UploadRunContent } from "@customTypes/Content"; | 29 | import { UploadRunContent } from '@customTypes/Content'; |
| 30 | 30 | ||
| 31 | // add new api call function entries here | 31 | // add new api call function entries here |
| 32 | // example usage: API.get_games(); | 32 | // example usage: API.get_games(); |
| @@ -91,7 +91,7 @@ export const API = { | |||
| 91 | delete_map_summary(token, map_id, route_id), | 91 | delete_map_summary(token, map_id, route_id), |
| 92 | }; | 92 | }; |
| 93 | 93 | ||
| 94 | const BASE_API_URL: string = "/api/v1/"; | 94 | const BASE_API_URL: string = '/api/v1/'; |
| 95 | 95 | ||
| 96 | export function url(path: string): string { | 96 | export function url(path: string): string { |
| 97 | return BASE_API_URL + path; | 97 | return BASE_API_URL + path; |
diff --git a/frontend/src/api/Auth.ts b/frontend/src/api/Auth.ts index 875c7e5..e495d47 100644 --- a/frontend/src/api/Auth.ts +++ b/frontend/src/api/Auth.ts | |||
| @@ -1,8 +1,8 @@ | |||
| 1 | import axios from "axios"; | 1 | 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 | } |
| @@ -10,5 +10,5 @@ export const get_token = async (): Promise<string | undefined> => { | |||
| 10 | }; | 10 | }; |
| 11 | 11 | ||
| 12 | export const delete_token = async () => { | 12 | export const delete_token = async () => { |
| 13 | await axios.delete(url("token")); | 13 | await axios.delete(url('token')); |
| 14 | }; | 14 | }; |
diff --git a/frontend/src/api/Games.ts b/frontend/src/api/Games.ts index 72bb4b3..0e47091 100644 --- a/frontend/src/api/Games.ts +++ b/frontend/src/api/Games.ts | |||
| @@ -1,31 +1,35 @@ | |||
| 1 | import axios from "axios"; | 1 | import axios from 'axios'; |
| 2 | import { url } from "@api/Api"; | 2 | import { url } from '@api/Api'; |
| 3 | import { GameChapter, GamesChapters } from "@customTypes/Chapters"; | 3 | import { GameChapter, GamesChapters } from '@customTypes/Chapters'; |
| 4 | import { Game } from "@customTypes/Game"; | 4 | import { Game } from '@customTypes/Game'; |
| 5 | import { Map } from "@customTypes/Map"; | 5 | 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 | ||
| 13 | export const get_chapters = async (chapter_id: string): Promise<GameChapter> => { | 13 | export const get_chapters = async ( |
| 14 | chapter_id: string | ||
| 15 | ): Promise<GameChapter> => { | ||
| 14 | const response = await axios.get(url(`chapters/${chapter_id}`)); | 16 | const response = await axios.get(url(`chapters/${chapter_id}`)); |
| 15 | return response.data.data; | 17 | return response.data.data; |
| 16 | } | 18 | }; |
| 17 | 19 | ||
| 18 | export const get_games_chapters = async (game_id: string): Promise<GamesChapters> => { | 20 | export const get_games_chapters = async ( |
| 21 | game_id: string | ||
| 22 | ): Promise<GamesChapters> => { | ||
| 19 | const response = await axios.get(url(`games/${game_id}`)); | 23 | const response = await axios.get(url(`games/${game_id}`)); |
| 20 | return response.data.data; | 24 | return response.data.data; |
| 21 | }; | 25 | }; |
| 22 | 26 | ||
| 23 | export const get_game_maps = async (game_id: string): Promise<Map[]> => { | 27 | export const get_game_maps = async (game_id: string): Promise<Map[]> => { |
| 24 | const response = await axios.get(url(`games/${game_id}/maps`)) | 28 | const response = await axios.get(url(`games/${game_id}/maps`)); |
| 25 | return response.data.data.maps; | 29 | return response.data.data.maps; |
| 26 | }; | 30 | }; |
| 27 | 31 | ||
| 28 | export const get_search = async (q: string): Promise<Search> => { | 32 | export const get_search = async (q: string): Promise<Search> => { |
| 29 | const response = await axios.get(url(`search?q=${q}`)) | 33 | const response = await axios.get(url(`search?q=${q}`)); |
| 30 | return response.data.data; | 34 | return response.data.data; |
| 31 | }; | 35 | }; |
diff --git a/frontend/src/api/Maps.ts b/frontend/src/api/Maps.ts index aa967ce..3a22f88 100644 --- a/frontend/src/api/Maps.ts +++ b/frontend/src/api/Maps.ts | |||
| @@ -1,15 +1,25 @@ | |||
| 1 | import axios from "axios"; | 1 | import axios from 'axios'; |
| 2 | import { url } from "@api/Api"; | 2 | import { url } from '@api/Api'; |
| 3 | import { MapDiscussionContent, UploadRunContent } from "@customTypes/Content"; | 3 | import { MapDiscussionContent, UploadRunContent } from '@customTypes/Content'; |
| 4 | import { MapSummary, MapLeaderboard, MapDiscussions, MapDiscussion } from "@customTypes/Map"; | 4 | import { |
| 5 | MapSummary, | ||
| 6 | MapLeaderboard, | ||
| 7 | MapDiscussions, | ||
| 8 | MapDiscussion, | ||
| 9 | } from '@customTypes/Map'; | ||
| 5 | 10 | ||
| 6 | export const get_map_summary = async (map_id: string): Promise<MapSummary> => { | 11 | export const get_map_summary = async (map_id: string): Promise<MapSummary> => { |
| 7 | const response = await axios.get(url(`maps/${map_id}/summary`)) | 12 | const response = await axios.get(url(`maps/${map_id}/summary`)); |
| 8 | return response.data.data; | 13 | return response.data.data; |
| 9 | }; | 14 | }; |
| 10 | 15 | ||
| 11 | export const get_map_leaderboard = async (map_id: string, page: string): Promise<MapLeaderboard | undefined> => { | 16 | export const get_map_leaderboard = async ( |
| 12 | const response = await axios.get(url(`maps/${map_id}/leaderboards?page=${page}`)); | 17 | map_id: string, |
| 18 | page: string | ||
| 19 | ): Promise<MapLeaderboard | undefined> => { | ||
| 20 | const response = await axios.get( | ||
| 21 | url(`maps/${map_id}/leaderboards?page=${page}`) | ||
| 22 | ); | ||
| 13 | if (!response.data.success) { | 23 | if (!response.data.success) { |
| 14 | return undefined; | 24 | return undefined; |
| 15 | } | 25 | } |
| @@ -25,7 +35,9 @@ export const get_map_leaderboard = async (map_id: string, page: string): Promise | |||
| 25 | return data; | 35 | return data; |
| 26 | }; | 36 | }; |
| 27 | 37 | ||
| 28 | export const get_map_discussions = async (map_id: string): Promise<MapDiscussions | undefined> => { | 38 | export const get_map_discussions = async ( |
| 39 | map_id: string | ||
| 40 | ): Promise<MapDiscussions | undefined> => { | ||
| 29 | const response = await axios.get(url(`maps/${map_id}/discussions`)); | 41 | const response = await axios.get(url(`maps/${map_id}/discussions`)); |
| 30 | if (!response.data.data.discussions) { | 42 | if (!response.data.data.discussions) { |
| 31 | return undefined; | 43 | return undefined; |
| @@ -33,74 +45,122 @@ export const get_map_discussions = async (map_id: string): Promise<MapDiscussion | |||
| 33 | return response.data.data; | 45 | return response.data.data; |
| 34 | }; | 46 | }; |
| 35 | 47 | ||
| 36 | export const get_map_discussion = async (map_id: string, discussion_id: number): Promise<MapDiscussion | undefined> => { | 48 | export const get_map_discussion = async ( |
| 37 | const response = await axios.get(url(`maps/${map_id}/discussions/${discussion_id}`)); | 49 | map_id: string, |
| 50 | discussion_id: number | ||
| 51 | ): Promise<MapDiscussion | undefined> => { | ||
| 52 | const response = await axios.get( | ||
| 53 | url(`maps/${map_id}/discussions/${discussion_id}`) | ||
| 54 | ); | ||
| 38 | if (!response.data.data.discussion) { | 55 | if (!response.data.data.discussion) { |
| 39 | return undefined; | 56 | return undefined; |
| 40 | } | 57 | } |
| 41 | return response.data.data; | 58 | return response.data.data; |
| 42 | }; | 59 | }; |
| 43 | 60 | ||
| 44 | export const post_map_discussion = async (token: string, map_id: string, content: MapDiscussionContent): Promise<boolean> => { | 61 | export const post_map_discussion = async ( |
| 45 | const response = await axios.post(url(`maps/${map_id}/discussions`), { | 62 | token: string, |
| 46 | "title": content.title, | 63 | map_id: string, |
| 47 | "content": content.content, | 64 | content: MapDiscussionContent |
| 48 | }, { | 65 | ): Promise<boolean> => { |
| 49 | headers: { | 66 | const response = await axios.post( |
| 50 | "Authorization": token, | 67 | url(`maps/${map_id}/discussions`), |
| 68 | { | ||
| 69 | title: content.title, | ||
| 70 | content: content.content, | ||
| 71 | }, | ||
| 72 | { | ||
| 73 | headers: { | ||
| 74 | Authorization: token, | ||
| 75 | }, | ||
| 51 | } | 76 | } |
| 52 | }); | 77 | ); |
| 53 | return response.data.success; | 78 | return response.data.success; |
| 54 | }; | 79 | }; |
| 55 | 80 | ||
| 56 | export const post_map_discussion_comment = async (token: string, map_id: string, discussion_id: number, comment: string): Promise<boolean> => { | 81 | export const post_map_discussion_comment = async ( |
| 57 | const response = await axios.post(url(`maps/${map_id}/discussions/${discussion_id}`), { | 82 | token: string, |
| 58 | "comment": comment, | 83 | map_id: string, |
| 59 | }, { | 84 | discussion_id: number, |
| 60 | headers: { | 85 | comment: string |
| 61 | "Authorization": token, | 86 | ): Promise<boolean> => { |
| 87 | const response = await axios.post( | ||
| 88 | url(`maps/${map_id}/discussions/${discussion_id}`), | ||
| 89 | { | ||
| 90 | comment: comment, | ||
| 91 | }, | ||
| 92 | { | ||
| 93 | headers: { | ||
| 94 | Authorization: token, | ||
| 95 | }, | ||
| 62 | } | 96 | } |
| 63 | }); | 97 | ); |
| 64 | return response.data.success; | 98 | return response.data.success; |
| 65 | }; | 99 | }; |
| 66 | 100 | ||
| 67 | export const delete_map_discussion = async (token: string, map_id: string, discussion_id: number): Promise<boolean> => { | 101 | export const delete_map_discussion = async ( |
| 68 | const response = await axios.delete(url(`maps/${map_id}/discussions/${discussion_id}`), { | 102 | token: string, |
| 69 | headers: { | 103 | map_id: string, |
| 70 | "Authorization": token, | 104 | discussion_id: number |
| 105 | ): Promise<boolean> => { | ||
| 106 | const response = await axios.delete( | ||
| 107 | url(`maps/${map_id}/discussions/${discussion_id}`), | ||
| 108 | { | ||
| 109 | headers: { | ||
| 110 | Authorization: token, | ||
| 111 | }, | ||
| 71 | } | 112 | } |
| 72 | }); | 113 | ); |
| 73 | return response.data.success; | 114 | return response.data.success; |
| 74 | }; | 115 | }; |
| 75 | 116 | ||
| 76 | export const post_record = async (token: string, run: UploadRunContent, map_id: number): Promise<[boolean, string]> => { | 117 | export const post_record = async ( |
| 118 | token: string, | ||
| 119 | run: UploadRunContent, | ||
| 120 | map_id: number | ||
| 121 | ): Promise<[boolean, string]> => { | ||
| 77 | if (run.partner_demo) { | 122 | if (run.partner_demo) { |
| 78 | const response = await axios.postForm(url(`maps/${map_id}/record`), { | 123 | const response = await axios.postForm( |
| 79 | "host_demo": run.host_demo, | 124 | url(`maps/${map_id}/record`), |
| 80 | "partner_demo": run.partner_demo, | 125 | { |
| 81 | }, { | 126 | host_demo: run.host_demo, |
| 82 | headers: { | 127 | partner_demo: run.partner_demo, |
| 83 | "Authorization": token, | 128 | }, |
| 129 | { | ||
| 130 | headers: { | ||
| 131 | Authorization: token, | ||
| 132 | }, | ||
| 84 | } | 133 | } |
| 85 | }); | 134 | ); |
| 86 | return [response.data.success, response.data.message]; | 135 | return [response.data.success, response.data.message]; |
| 87 | } else { | 136 | } else { |
| 88 | const response = await axios.postForm(url(`maps/${map_id}/record`), { | 137 | const response = await axios.postForm( |
| 89 | "host_demo": run.host_demo, | 138 | url(`maps/${map_id}/record`), |
| 90 | }, { | 139 | { |
| 91 | headers: { | 140 | host_demo: run.host_demo, |
| 92 | "Authorization": token, | 141 | }, |
| 142 | { | ||
| 143 | headers: { | ||
| 144 | Authorization: token, | ||
| 145 | }, | ||
| 93 | } | 146 | } |
| 94 | }); | 147 | ); |
| 95 | return [response.data.success, response.data.message]; | 148 | return [response.data.success, response.data.message]; |
| 96 | } | 149 | } |
| 97 | } | 150 | }; |
| 98 | 151 | ||
| 99 | export const delete_map_record = async (token: string, map_id: number, record_id: number): Promise<boolean> => { | 152 | export const delete_map_record = async ( |
| 100 | const response = await axios.delete(url(`maps/${map_id}/record/${record_id}`), { | 153 | token: string, |
| 101 | headers: { | 154 | map_id: number, |
| 102 | "Authorization": token, | 155 | record_id: number |
| 156 | ): Promise<boolean> => { | ||
| 157 | const response = await axios.delete( | ||
| 158 | url(`maps/${map_id}/record/${record_id}`), | ||
| 159 | { | ||
| 160 | headers: { | ||
| 161 | Authorization: token, | ||
| 162 | }, | ||
| 103 | } | 163 | } |
| 104 | }); | 164 | ); |
| 105 | return response.data.success; | 165 | return response.data.success; |
| 106 | }; | 166 | }; |
diff --git a/frontend/src/api/Mod.ts b/frontend/src/api/Mod.ts index 1511f8b..69e76c5 100644 --- a/frontend/src/api/Mod.ts +++ b/frontend/src/api/Mod.ts | |||
| @@ -1,58 +1,86 @@ | |||
| 1 | import axios from "axios"; | 1 | import axios from 'axios'; |
| 2 | import { url } from "@api/Api"; | 2 | import { url } from '@api/Api'; |
| 3 | import { ModMenuContent } from "@customTypes/Content"; | 3 | import { ModMenuContent } from '@customTypes/Content'; |
| 4 | 4 | ||
| 5 | export const put_map_image = async (token: string, map_id: string, image: string): Promise<boolean> => { | 5 | export const put_map_image = async ( |
| 6 | const response = await axios.put(url(`maps/${map_id}/image`), { | 6 | token: string, |
| 7 | "image": image, | 7 | map_id: string, |
| 8 | }, { | 8 | image: string |
| 9 | headers: { | 9 | ): Promise<boolean> => { |
| 10 | "Authorization": token, | 10 | const response = await axios.put( |
| 11 | url(`maps/${map_id}/image`), | ||
| 12 | { | ||
| 13 | image: image, | ||
| 14 | }, | ||
| 15 | { | ||
| 16 | headers: { | ||
| 17 | Authorization: token, | ||
| 18 | }, | ||
| 11 | } | 19 | } |
| 12 | }); | 20 | ); |
| 13 | return response.data.success; | 21 | return response.data.success; |
| 14 | }; | 22 | }; |
| 15 | 23 | ||
| 16 | export const post_map_summary = async (token: string, map_id: string, content: ModMenuContent): Promise<boolean> => { | 24 | export const post_map_summary = async ( |
| 17 | const response = await axios.post(url(`maps/${map_id}/summary`), { | 25 | token: string, |
| 18 | "category_id": content.category_id, | 26 | map_id: string, |
| 19 | "user_name": content.name, | 27 | content: ModMenuContent |
| 20 | "score_count": content.score, | 28 | ): Promise<boolean> => { |
| 21 | "record_date": content.date, | 29 | const response = await axios.post( |
| 22 | "showcase": content.showcase, | 30 | url(`maps/${map_id}/summary`), |
| 23 | "description": content.description, | 31 | { |
| 24 | }, { | 32 | category_id: content.category_id, |
| 25 | headers: { | 33 | user_name: content.name, |
| 26 | "Authorization": token, | 34 | score_count: content.score, |
| 35 | record_date: content.date, | ||
| 36 | showcase: content.showcase, | ||
| 37 | description: content.description, | ||
| 38 | }, | ||
| 39 | { | ||
| 40 | headers: { | ||
| 41 | Authorization: token, | ||
| 42 | }, | ||
| 27 | } | 43 | } |
| 28 | }); | 44 | ); |
| 29 | return response.data.success; | 45 | return response.data.success; |
| 30 | }; | 46 | }; |
| 31 | 47 | ||
| 32 | export const put_map_summary = async (token: string, map_id: string, content: ModMenuContent): Promise<boolean> => { | 48 | export const put_map_summary = async ( |
| 33 | const response = await axios.put(url(`maps/${map_id}/summary`), { | 49 | token: string, |
| 34 | "route_id": content.id, | 50 | map_id: string, |
| 35 | "user_name": content.name, | 51 | content: ModMenuContent |
| 36 | "score_count": content.score, | 52 | ): Promise<boolean> => { |
| 37 | "record_date": content.date, | 53 | const response = await axios.put( |
| 38 | "showcase": content.showcase, | 54 | url(`maps/${map_id}/summary`), |
| 39 | "description": content.description, | 55 | { |
| 40 | }, { | 56 | route_id: content.id, |
| 41 | headers: { | 57 | user_name: content.name, |
| 42 | "Authorization": token, | 58 | score_count: content.score, |
| 59 | record_date: content.date, | ||
| 60 | showcase: content.showcase, | ||
| 61 | description: content.description, | ||
| 62 | }, | ||
| 63 | { | ||
| 64 | headers: { | ||
| 65 | Authorization: token, | ||
| 66 | }, | ||
| 43 | } | 67 | } |
| 44 | }); | 68 | ); |
| 45 | return response.data.success; | 69 | return response.data.success; |
| 46 | }; | 70 | }; |
| 47 | 71 | ||
| 48 | export const delete_map_summary = async (token: string, map_id: string, route_id: number): Promise<boolean> => { | 72 | export const delete_map_summary = async ( |
| 73 | token: string, | ||
| 74 | map_id: string, | ||
| 75 | route_id: number | ||
| 76 | ): Promise<boolean> => { | ||
| 49 | const response = await axios.delete(url(`maps/${map_id}/summary`), { | 77 | const response = await axios.delete(url(`maps/${map_id}/summary`), { |
| 50 | data: { | 78 | data: { |
| 51 | "route_id": route_id, | 79 | route_id: route_id, |
| 52 | }, | 80 | }, |
| 53 | headers: { | 81 | headers: { |
| 54 | "Authorization": token, | 82 | Authorization: token, |
| 55 | } | 83 | }, |
| 56 | }); | 84 | }); |
| 57 | return response.data.success; | 85 | return response.data.success; |
| 58 | }; | 86 | }; |
diff --git a/frontend/src/api/Rankings.ts b/frontend/src/api/Rankings.ts index b8d9bec..9afd999 100644 --- a/frontend/src/api/Rankings.ts +++ b/frontend/src/api/Rankings.ts | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | import axios from "axios"; | 1 | import axios from 'axios'; |
| 2 | import { url } from "@api/Api"; | 2 | 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`)); |
diff --git a/frontend/src/api/User.ts b/frontend/src/api/User.ts index 004aa22..4ce21e1 100644 --- a/frontend/src/api/User.ts +++ b/frontend/src/api/User.ts | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | import axios from "axios"; | 1 | import axios from 'axios'; |
| 2 | import { url } from "@api/Api"; | 2 | import { url } from '@api/Api'; |
| 3 | import { UserProfile } from "@customTypes/Profile"; | 3 | import { UserProfile } from '@customTypes/Profile'; |
| 4 | 4 | ||
| 5 | export const get_user = async (user_id: string): Promise<UserProfile> => { | 5 | export const get_user = async (user_id: string): Promise<UserProfile> => { |
| 6 | const response = await axios.get(url(`users/${user_id}`)); | 6 | const response = await axios.get(url(`users/${user_id}`)); |
diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx index 44a653b..925118d 100644 --- a/frontend/src/components/ConfirmDialog.tsx +++ b/frontend/src/components/ConfirmDialog.tsx | |||
| @@ -1,31 +1,36 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | 2 | ||
| 3 | import "@css/Dialog.css" | 3 | import '@css/Dialog.css'; |
| 4 | 4 | ||
| 5 | interface ConfirmDialogProps { | 5 | interface ConfirmDialogProps { |
| 6 | title: string; | 6 | title: string; |
| 7 | subtitle: string; | 7 | subtitle: string; |
| 8 | onConfirm: () => void; | 8 | onConfirm: () => void; |
| 9 | onCancel: () => void; | 9 | onCancel: () => void; |
| 10 | }; | 10 | } |
| 11 | 11 | ||
| 12 | const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ title, subtitle, onConfirm, onCancel }) => { | 12 | const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ |
| 13 | return ( | 13 | title, |
| 14 | <div className='dimmer'> | 14 | subtitle, |
| 15 | <div className='dialog'> | 15 | onConfirm, |
| 16 | <div className='dialog-element dialog-header'> | 16 | onCancel, |
| 17 | <span>{title}</span> | 17 | }) => { |
| 18 | </div> | 18 | return ( |
| 19 | <div className='dialog-element dialog-description'> | 19 | <div className="dimmer"> |
| 20 | <span>{subtitle}</span> | 20 | <div className="dialog"> |
| 21 | </div> | 21 | <div className="dialog-element dialog-header"> |
| 22 | <div className='dialog-element dialog-btns-container'> | 22 | <span>{title}</span> |
| 23 | <button onClick={onCancel}>Cancel</button> | 23 | </div> |
| 24 | <button onClick={onConfirm}>Confirm</button> | 24 | <div className="dialog-element dialog-description"> |
| 25 | </div> | 25 | <span>{subtitle}</span> |
| 26 | </div> | 26 | </div> |
| 27 | <div className="dialog-element dialog-btns-container"> | ||
| 28 | <button onClick={onCancel}>Cancel</button> | ||
| 29 | <button onClick={onConfirm}>Confirm</button> | ||
| 27 | </div> | 30 | </div> |
| 28 | ) | 31 | </div> |
| 32 | </div> | ||
| 33 | ); | ||
| 29 | }; | 34 | }; |
| 30 | 35 | ||
| 31 | export default ConfirmDialog; | 36 | export default ConfirmDialog; |
diff --git a/frontend/src/components/Discussions.tsx b/frontend/src/components/Discussions.tsx index 62a9fc7..994cd8e 100644 --- a/frontend/src/components/Discussions.tsx +++ b/frontend/src/components/Discussions.tsx | |||
| @@ -1,16 +1,16 @@ | |||
| 1 | import React from "react"; | 1 | import React from 'react'; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | MapDiscussion, | 4 | MapDiscussion, |
| 5 | MapDiscussions, | 5 | MapDiscussions, |
| 6 | MapDiscussionsDetail, | 6 | MapDiscussionsDetail, |
| 7 | } from "@customTypes/Map"; | 7 | } from '@customTypes/Map'; |
| 8 | import { MapDiscussionContent } from "@customTypes/Content"; | 8 | import { MapDiscussionContent } from '@customTypes/Content'; |
| 9 | import { time_ago } from "@utils/Time"; | 9 | import { time_ago } from '@utils/Time'; |
| 10 | import { API } from "@api/Api"; | 10 | import { API } from '@api/Api'; |
| 11 | import "@css/Maps.css"; | 11 | import '@css/Maps.css'; |
| 12 | import { Link } from "react-router-dom"; | 12 | import { Link } from 'react-router-dom'; |
| 13 | import useConfirm from "@hooks/UseConfirm"; | 13 | import useConfirm from '@hooks/UseConfirm'; |
| 14 | 14 | ||
| 15 | interface DiscussionsProps { | 15 | interface DiscussionsProps { |
| 16 | token?: string; | 16 | token?: string; |
| @@ -32,17 +32,17 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 32 | const [discussionThread, setDiscussionThread] = React.useState< | 32 | const [discussionThread, setDiscussionThread] = React.useState< |
| 33 | MapDiscussion | undefined | 33 | MapDiscussion | undefined |
| 34 | >(undefined); | 34 | >(undefined); |
| 35 | const [discussionSearch, setDiscussionSearch] = React.useState<string>(""); | 35 | const [discussionSearch, setDiscussionSearch] = React.useState<string>(''); |
| 36 | 36 | ||
| 37 | const [createDiscussion, setCreateDiscussion] = | 37 | const [createDiscussion, setCreateDiscussion] = |
| 38 | React.useState<boolean>(false); | 38 | React.useState<boolean>(false); |
| 39 | const [createDiscussionContent, setCreateDiscussionContent] = | 39 | const [createDiscussionContent, setCreateDiscussionContent] = |
| 40 | React.useState<MapDiscussionContent>({ | 40 | React.useState<MapDiscussionContent>({ |
| 41 | title: "", | 41 | title: '', |
| 42 | content: "", | 42 | content: '', |
| 43 | }); | 43 | }); |
| 44 | const [createDiscussionCommentContent, setCreateDiscussionCommentContent] = | 44 | const [createDiscussionCommentContent, setCreateDiscussionCommentContent] = |
| 45 | React.useState<string>(""); | 45 | React.useState<string>(''); |
| 46 | 46 | ||
| 47 | const _open_map_discussion = async (discussion_id: number) => { | 47 | const _open_map_discussion = async (discussion_id: number) => { |
| 48 | const mapDiscussion = await API.get_map_discussion(mapID, discussion_id); | 48 | const mapDiscussion = await API.get_map_discussion(mapID, discussion_id); |
| @@ -72,7 +72,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 72 | const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => { | 72 | const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => { |
| 73 | if ( | 73 | if ( |
| 74 | await confirm( | 74 | await confirm( |
| 75 | "Delete Map Discussion", | 75 | 'Delete Map Discussion', |
| 76 | `Are you sure you want to remove post: ${discussion.title}?` | 76 | `Are you sure you want to remove post: ${discussion.title}?` |
| 77 | ) | 77 | ) |
| 78 | ) { | 78 | ) { |
| @@ -90,8 +90,8 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 90 | <input | 90 | <input |
| 91 | type="text" | 91 | type="text" |
| 92 | value={discussionSearch} | 92 | value={discussionSearch} |
| 93 | placeholder={"Search for posts..."} | 93 | placeholder={'Search for posts...'} |
| 94 | onChange={(e) => setDiscussionSearch(e.target.value)} | 94 | onChange={e => setDiscussionSearch(e.target.value)} |
| 95 | /> | 95 | /> |
| 96 | <div> | 96 | <div> |
| 97 | <button onClick={() => setCreateDiscussion(true)}>New Post</button> | 97 | <button onClick={() => setCreateDiscussion(true)}>New Post</button> |
| @@ -104,11 +104,11 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 104 | <div id="discussion-create"> | 104 | <div id="discussion-create"> |
| 105 | <span>Create Post</span> | 105 | <span>Create Post</span> |
| 106 | <button onClick={() => setCreateDiscussion(false)}>X</button> | 106 | <button onClick={() => setCreateDiscussion(false)}>X</button> |
| 107 | <div style={{ gridColumn: "1 / span 2" }}> | 107 | <div style={{ gridColumn: '1 / span 2' }}> |
| 108 | <input | 108 | <input |
| 109 | id="discussion-create-title" | 109 | id="discussion-create-title" |
| 110 | placeholder="Title..." | 110 | placeholder="Title..." |
| 111 | onChange={(e) => | 111 | onChange={e => |
| 112 | setCreateDiscussionContent({ | 112 | setCreateDiscussionContent({ |
| 113 | ...createDiscussionContent, | 113 | ...createDiscussionContent, |
| 114 | title: e.target.value, | 114 | title: e.target.value, |
| @@ -118,7 +118,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 118 | <input | 118 | <input |
| 119 | id="discussion-create-content" | 119 | id="discussion-create-content" |
| 120 | placeholder="Enter the content..." | 120 | placeholder="Enter the content..." |
| 121 | onChange={(e) => | 121 | onChange={e => |
| 122 | setCreateDiscussionContent({ | 122 | setCreateDiscussionContent({ |
| 123 | ...createDiscussionContent, | 123 | ...createDiscussionContent, |
| 124 | content: e.target.value, | 124 | content: e.target.value, |
| @@ -126,7 +126,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 126 | } | 126 | } |
| 127 | /> | 127 | /> |
| 128 | </div> | 128 | </div> |
| 129 | <div style={{ placeItems: "end", gridColumn: "1 / span 2" }}> | 129 | <div style={{ placeItems: 'end', gridColumn: '1 / span 2' }}> |
| 130 | <button | 130 | <button |
| 131 | id="discussion-create-button" | 131 | id="discussion-create-button" |
| 132 | onClick={() => _create_map_discussion()} | 132 | onClick={() => _create_map_discussion()} |
| @@ -157,8 +157,8 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 157 | {time_ago( | 157 | {time_ago( |
| 158 | new Date( | 158 | new Date( |
| 159 | discussionThread.discussion.created_at | 159 | discussionThread.discussion.created_at |
| 160 | .replace("T", " ") | 160 | .replace('T', ' ') |
| 161 | .replace("Z", "") | 161 | .replace('Z', '') |
| 162 | ) | 162 | ) |
| 163 | )} | 163 | )} |
| 164 | </span> | 164 | </span> |
| @@ -170,7 +170,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 170 | (a, b) => | 170 | (a, b) => |
| 171 | new Date(a.date).getTime() - new Date(b.date).getTime() | 171 | new Date(a.date).getTime() - new Date(b.date).getTime() |
| 172 | ) | 172 | ) |
| 173 | .map((e) => ( | 173 | .map(e => ( |
| 174 | <> | 174 | <> |
| 175 | <Link to={`/users/${e.user.steam_id}`}> | 175 | <Link to={`/users/${e.user.steam_id}`}> |
| 176 | <img src={e.user.avatar_link} alt="" /> | 176 | <img src={e.user.avatar_link} alt="" /> |
| @@ -180,7 +180,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 180 | <span> | 180 | <span> |
| 181 | {time_ago( | 181 | {time_ago( |
| 182 | new Date( | 182 | new Date( |
| 183 | e.date.replace("T", " ").replace("Z", "") | 183 | e.date.replace('T', ' ').replace('Z', '') |
| 184 | ) | 184 | ) |
| 185 | )} | 185 | )} |
| 186 | </span> | 186 | </span> |
| @@ -188,29 +188,29 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 188 | </div> | 188 | </div> |
| 189 | </> | 189 | </> |
| 190 | )) | 190 | )) |
| 191 | : ""} | 191 | : ''} |
| 192 | </div> | 192 | </div> |
| 193 | <div id="discussion-send"> | 193 | <div id="discussion-send"> |
| 194 | <input | 194 | <input |
| 195 | type="text" | 195 | type="text" |
| 196 | value={createDiscussionCommentContent} | 196 | value={createDiscussionCommentContent} |
| 197 | placeholder={"Message"} | 197 | placeholder={'Message'} |
| 198 | onKeyDown={(e) => | 198 | onKeyDown={e => |
| 199 | e.key === "Enter" && | 199 | e.key === 'Enter' && |
| 200 | _create_map_discussion_comment(discussionThread.discussion.id) | 200 | _create_map_discussion_comment(discussionThread.discussion.id) |
| 201 | } | 201 | } |
| 202 | onChange={(e) => | 202 | onChange={e => |
| 203 | setCreateDiscussionCommentContent(e.target.value) | 203 | setCreateDiscussionCommentContent(e.target.value) |
| 204 | } | 204 | } |
| 205 | /> | 205 | /> |
| 206 | <div> | 206 | <div> |
| 207 | <button | 207 | <button |
| 208 | onClick={() => { | 208 | onClick={() => { |
| 209 | if (createDiscussionCommentContent !== "") { | 209 | if (createDiscussionCommentContent !== '') { |
| 210 | _create_map_discussion_comment( | 210 | _create_map_discussion_comment( |
| 211 | discussionThread.discussion.id | 211 | discussionThread.discussion.id |
| 212 | ); | 212 | ); |
| 213 | setCreateDiscussionCommentContent(""); | 213 | setCreateDiscussionCommentContent(''); |
| 214 | } | 214 | } |
| 215 | }} | 215 | }} |
| 216 | > | 216 | > |
| @@ -222,7 +222,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 222 | ) : data ? ( | 222 | ) : data ? ( |
| 223 | <> | 223 | <> |
| 224 | {data.discussions | 224 | {data.discussions |
| 225 | .filter((f) => f.title.includes(discussionSearch)) | 225 | .filter(f => f.title.includes(discussionSearch)) |
| 226 | .sort( | 226 | .sort( |
| 227 | (a, b) => | 227 | (a, b) => |
| 228 | new Date(b.updated_at).getTime() - | 228 | new Date(b.updated_at).getTime() - |
| @@ -234,7 +234,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 234 | <span>{e.title}</span> | 234 | <span>{e.title}</span> |
| 235 | {isModerator ? ( | 235 | {isModerator ? ( |
| 236 | <button | 236 | <button |
| 237 | onClick={(m) => { | 237 | onClick={m => { |
| 238 | m.stopPropagation(); | 238 | m.stopPropagation(); |
| 239 | _delete_map_discussion(e); | 239 | _delete_map_discussion(e); |
| 240 | }} | 240 | }} |
| @@ -248,10 +248,10 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 248 | <b>{e.creator.user_name}:</b> {e.content} | 248 | <b>{e.creator.user_name}:</b> {e.content} |
| 249 | </span> | 249 | </span> |
| 250 | <span> | 250 | <span> |
| 251 | Last Updated:{" "} | 251 | Last Updated:{' '} |
| 252 | {time_ago( | 252 | {time_ago( |
| 253 | new Date( | 253 | new Date( |
| 254 | e.updated_at.replace("T", " ").replace("Z", "") | 254 | e.updated_at.replace('T', ' ').replace('Z', '') |
| 255 | ) | 255 | ) |
| 256 | )} | 256 | )} |
| 257 | </span> | 257 | </span> |
| @@ -260,7 +260,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 260 | ))} | 260 | ))} |
| 261 | </> | 261 | </> |
| 262 | ) : ( | 262 | ) : ( |
| 263 | <span style={{ textAlign: "center", display: "block" }}> | 263 | <span style={{ textAlign: 'center', display: 'block' }}> |
| 264 | No Discussions... | 264 | No Discussions... |
| 265 | </span> | 265 | </span> |
| 266 | ) | 266 | ) |
diff --git a/frontend/src/components/GameCategory.tsx b/frontend/src/components/GameCategory.tsx index d8879ef..b13be48 100644 --- a/frontend/src/components/GameCategory.tsx +++ b/frontend/src/components/GameCategory.tsx | |||
| @@ -1,24 +1,31 @@ | |||
| 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 { |
| 8 | game: Game; | 8 | game: Game; |
| 9 | cat: GameCategoryPortals; | 9 | cat: GameCategoryPortals; |
| 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 |
| 15 | <div> | 15 | className="games-page-item-body-item" |
| 16 | <span className='games-page-item-body-item-title'>{cat.category.name}</span> | 16 | to={'/games/' + game.id + '?cat=' + cat.category.id} |
| 17 | <br /> | 17 | > |
| 18 | <span className='games-page-item-body-item-num'>{cat.portal_count}</span> | 18 | <div> |
| 19 | </div> | 19 | <span className="games-page-item-body-item-title"> |
| 20 | </Link> | 20 | {cat.category.name} |
| 21 | ) | 21 | </span> |
| 22 | } | 22 | <br /> |
| 23 | <span className="games-page-item-body-item-num"> | ||
| 24 | {cat.portal_count} | ||
| 25 | </span> | ||
| 26 | </div> | ||
| 27 | </Link> | ||
| 28 | ); | ||
| 29 | }; | ||
| 23 | 30 | ||
| 24 | export default GameCategory; | 31 | export default GameCategory; |
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx index 3bd2842..b58bbdd 100644 --- a/frontend/src/components/GameEntry.tsx +++ b/frontend/src/components/GameEntry.tsx | |||
| @@ -1,8 +1,8 @@ | |||
| 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 | ||
| @@ -18,17 +18,26 @@ const GameEntry: React.FC<GameEntryProps> = ({ game }) => { | |||
| 18 | }, [game.category_portals]); | 18 | }, [game.category_portals]); |
| 19 | 19 | ||
| 20 | return ( | 20 | return ( |
| 21 | <Link to={"/games/" + game.id}><div className='games-page-item'> | 21 | <Link to={'/games/' + game.id}> |
| 22 | <div className='games-page-item-header'> | 22 | <div className="games-page-item"> |
| 23 | <div style={{ backgroundImage: `url(${game.image})` }} className='games-page-item-header-img'></div> | 23 | <div className="games-page-item-header"> |
| 24 | <span><b>{game.name}</b></span> | 24 | <div |
| 25 | style={{ backgroundImage: `url(${game.image})` }} | ||
| 26 | className="games-page-item-header-img" | ||
| 27 | ></div> | ||
| 28 | <span> | ||
| 29 | <b>{game.name}</b> | ||
| 30 | </span> | ||
| 31 | </div> | ||
| 32 | <div id={game.id as any as string} className="games-page-item-body"> | ||
| 33 | {catInfo.map((cat, index) => { | ||
| 34 | return ( | ||
| 35 | <GameCategory cat={cat} game={game} key={index}></GameCategory> | ||
| 36 | ); | ||
| 37 | })} | ||
| 38 | </div> | ||
| 25 | </div> | 39 | </div> |
| 26 | <div id={game.id as any as string} className='games-page-item-body'> | 40 | </Link> |
| 27 | {catInfo.map((cat, index) => { | ||
| 28 | return <GameCategory cat={cat} game={game} key={index}></GameCategory> | ||
| 29 | })} | ||
| 30 | </div> | ||
| 31 | </div></Link> | ||
| 32 | ); | 41 | ); |
| 33 | }; | 42 | }; |
| 34 | 43 | ||
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx index 05f01d0..fb72f2b 100644 --- a/frontend/src/components/Leaderboards.tsx +++ b/frontend/src/components/Leaderboards.tsx | |||
| @@ -1,12 +1,12 @@ | |||
| 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'; |
| 10 | 10 | ||
| 11 | interface LeaderboardsProps { | 11 | interface LeaderboardsProps { |
| 12 | mapID: string; | 12 | mapID: string; |
| @@ -35,7 +35,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 35 | if (!data) { | 35 | if (!data) { |
| 36 | return ( | 36 | return ( |
| 37 | <section id="section6" className="summary2"> | 37 | <section id="section6" className="summary2"> |
| 38 | <h1 style={{ textAlign: "center" }}> | 38 | <h1 style={{ textAlign: 'center' }}> |
| 39 | Map is not available for competitive boards. | 39 | Map is not available for competitive boards. |
| 40 | </h1> | 40 | </h1> |
| 41 | </section> | 41 | </section> |
| @@ -45,7 +45,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 45 | if (data.records.length === 0) { | 45 | if (data.records.length === 0) { |
| 46 | return ( | 46 | return ( |
| 47 | <section id="section6" className="summary2"> | 47 | <section id="section6" className="summary2"> |
| 48 | <h1 style={{ textAlign: "center" }}>No records found.</h1> | 48 | <h1 style={{ textAlign: 'center' }}>No records found.</h1> |
| 49 | </section> | 49 | </section> |
| 50 | ); | 50 | ); |
| 51 | } | 51 | } |
| @@ -58,8 +58,8 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 58 | id="leaderboard-top" | 58 | id="leaderboard-top" |
| 59 | style={ | 59 | style={ |
| 60 | data.map.is_coop | 60 | data.map.is_coop |
| 61 | ? { gridTemplateColumns: "7.5% 40% 7.5% 15% 15% 15%" } | 61 | ? { gridTemplateColumns: '7.5% 40% 7.5% 15% 15% 15%' } |
| 62 | : { gridTemplateColumns: "7.5% 30% 10% 20% 17.5% 15%" } | 62 | : { gridTemplateColumns: '7.5% 30% 10% 20% 17.5% 15%' } |
| 63 | } | 63 | } |
| 64 | > | 64 | > |
| 65 | <span>Place</span> | 65 | <span>Place</span> |
| @@ -82,13 +82,13 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 82 | onClick={() => | 82 | onClick={() => |
| 83 | pageNumber === 1 | 83 | pageNumber === 1 |
| 84 | ? null | 84 | ? null |
| 85 | : setPageNumber((prevPageNumber) => prevPageNumber - 1) | 85 | : setPageNumber(prevPageNumber => prevPageNumber - 1) |
| 86 | } | 86 | } |
| 87 | > | 87 | > |
| 88 | <i | 88 | <i |
| 89 | className="triangle" | 89 | className="triangle" |
| 90 | style={{ position: "relative", left: "-5px" }} | 90 | style={{ position: 'relative', left: '-5px' }} |
| 91 | ></i>{" "} | 91 | ></i>{' '} |
| 92 | </button> | 92 | </button> |
| 93 | <span> | 93 | <span> |
| 94 | {data.pagination.current_page}/{data.pagination.total_pages} | 94 | {data.pagination.current_page}/{data.pagination.total_pages} |
| @@ -97,17 +97,17 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 97 | onClick={() => | 97 | onClick={() => |
| 98 | pageNumber === data.pagination.total_pages | 98 | pageNumber === data.pagination.total_pages |
| 99 | ? null | 99 | ? null |
| 100 | : setPageNumber((prevPageNumber) => prevPageNumber + 1) | 100 | : setPageNumber(prevPageNumber => prevPageNumber + 1) |
| 101 | } | 101 | } |
| 102 | > | 102 | > |
| 103 | <i | 103 | <i |
| 104 | className="triangle" | 104 | className="triangle" |
| 105 | style={{ | 105 | style={{ |
| 106 | position: "relative", | 106 | position: 'relative', |
| 107 | left: "5px", | 107 | left: '5px', |
| 108 | transform: "rotate(180deg)", | 108 | transform: 'rotate(180deg)', |
| 109 | }} | 109 | }} |
| 110 | ></i>{" "} | 110 | ></i>{' '} |
| 111 | </button> | 111 | </button> |
| 112 | </div> | 112 | </div> |
| 113 | </div> | 113 | </div> |
| @@ -120,33 +120,33 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 120 | key={index} | 120 | key={index} |
| 121 | style={ | 121 | style={ |
| 122 | data.map.is_coop | 122 | data.map.is_coop |
| 123 | ? { gridTemplateColumns: "3% 4.5% 40% 4% 3.5% 15% 15% 14.5%" } | 123 | ? { gridTemplateColumns: '3% 4.5% 40% 4% 3.5% 15% 15% 14.5%' } |
| 124 | : { gridTemplateColumns: "3% 4.5% 30% 4% 6% 20% 17% 15%" } | 124 | : { gridTemplateColumns: '3% 4.5% 30% 4% 6% 20% 17% 15%' } |
| 125 | } | 125 | } |
| 126 | > | 126 | > |
| 127 | <span>{r.placement}</span> | 127 | <span>{r.placement}</span> |
| 128 | <span> </span> | 128 | <span> </span> |
| 129 | {r.kind === "multiplayer" ? ( | 129 | {r.kind === 'multiplayer' ? ( |
| 130 | <div> | 130 | <div> |
| 131 | <Link to={`/users/${r.host.steam_id}`}> | 131 | <Link to={`/users/${r.host.steam_id}`}> |
| 132 | <span> | 132 | <span> |
| 133 | <img src={r.host.avatar_link} alt="" /> {" "} | 133 | <img src={r.host.avatar_link} alt="" /> {' '} |
| 134 | {r.host.user_name} | 134 | {r.host.user_name} |
| 135 | </span> | 135 | </span> |
| 136 | </Link> | 136 | </Link> |
| 137 | <Link to={`/users/${r.partner.steam_id}`}> | 137 | <Link to={`/users/${r.partner.steam_id}`}> |
| 138 | <span> | 138 | <span> |
| 139 | <img src={r.partner.avatar_link} alt="" /> {" "} | 139 | <img src={r.partner.avatar_link} alt="" /> {' '} |
| 140 | {r.partner.user_name} | 140 | {r.partner.user_name} |
| 141 | </span> | 141 | </span> |
| 142 | </Link> | 142 | </Link> |
| 143 | </div> | 143 | </div> |
| 144 | ) : ( | 144 | ) : ( |
| 145 | r.kind === "singleplayer" && ( | 145 | r.kind === 'singleplayer' && ( |
| 146 | <div> | 146 | <div> |
| 147 | <Link to={`/users/${r.user.steam_id}`}> | 147 | <Link to={`/users/${r.user.steam_id}`}> |
| 148 | <span> | 148 | <span> |
| 149 | <img src={r.user.avatar_link} alt="" /> {" "} | 149 | <img src={r.user.avatar_link} alt="" /> {' '} |
| 150 | {r.user.user_name} | 150 | {r.user.user_name} |
| 151 | </span> | 151 | </span> |
| 152 | </Link> | 152 | </Link> |
| @@ -158,25 +158,25 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 158 | <span> </span> | 158 | <span> </span> |
| 159 | <span | 159 | <span |
| 160 | className="hover-popup" | 160 | className="hover-popup" |
| 161 | popup-text={r.score_time + " ticks"} | 161 | popup-text={r.score_time + ' ticks'} |
| 162 | > | 162 | > |
| 163 | {ticks_to_time(r.score_time)} | 163 | {ticks_to_time(r.score_time)} |
| 164 | </span> | 164 | </span> |
| 165 | <span | 165 | <span |
| 166 | className="hover-popup" | 166 | className="hover-popup" |
| 167 | popup-text={r.record_date.replace("T", " ").split(".")[0]} | 167 | popup-text={r.record_date.replace('T', ' ').split('.')[0]} |
| 168 | > | 168 | > |
| 169 | {time_ago( | 169 | {time_ago( |
| 170 | new Date(r.record_date.replace("T", " ").replace("Z", "")) | 170 | new Date(r.record_date.replace('T', ' ').replace('Z', '')) |
| 171 | )} | 171 | )} |
| 172 | </span> | 172 | </span> |
| 173 | 173 | ||
| 174 | {r.kind === "multiplayer" ? ( | 174 | {r.kind === 'multiplayer' ? ( |
| 175 | <span> | 175 | <span> |
| 176 | <button | 176 | <button |
| 177 | onClick={() => { | 177 | onClick={() => { |
| 178 | message( | 178 | message( |
| 179 | "Demo Information", | 179 | 'Demo Information', |
| 180 | `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}` | 180 | `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}` |
| 181 | ); | 181 | ); |
| 182 | }} | 182 | }} |
| @@ -193,7 +193,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 193 | alt="download" | 193 | alt="download" |
| 194 | style={{ | 194 | style={{ |
| 195 | filter: | 195 | filter: |
| 196 | "hue-rotate(160deg) contrast(60%) saturate(1000%)", | 196 | 'hue-rotate(160deg) contrast(60%) saturate(1000%)', |
| 197 | }} | 197 | }} |
| 198 | /> | 198 | /> |
| 199 | </button> | 199 | </button> |
| @@ -207,17 +207,17 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 207 | alt="download" | 207 | alt="download" |
| 208 | style={{ | 208 | style={{ |
| 209 | filter: | 209 | filter: |
| 210 | "hue-rotate(300deg) contrast(60%) saturate(1000%)", | 210 | 'hue-rotate(300deg) contrast(60%) saturate(1000%)', |
| 211 | }} | 211 | }} |
| 212 | /> | 212 | /> |
| 213 | </button> | 213 | </button> |
| 214 | </span> | 214 | </span> |
| 215 | ) : ( | 215 | ) : ( |
| 216 | r.kind === "singleplayer" && ( | 216 | r.kind === 'singleplayer' && ( |
| 217 | <span> | 217 | <span> |
| 218 | <button | 218 | <button |
| 219 | onClick={() => { | 219 | onClick={() => { |
| 220 | message("Demo Information", `Demo ID: ${r.demo_id}`); | 220 | message('Demo Information', `Demo ID: ${r.demo_id}`); |
| 221 | }} | 221 | }} |
| 222 | > | 222 | > |
| 223 | <img src={ThreedotIcon} alt="demo_id" /> | 223 | <img src={ThreedotIcon} alt="demo_id" /> |
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index f1628b2..093e406 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx | |||
| @@ -4,77 +4,78 @@ import { Link, useNavigate } from 'react-router-dom'; | |||
| 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 { |
| 10 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; | 10 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; |
| 11 | profile?: UserProfile; | 11 | profile?: UserProfile; |
| 12 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; | 12 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; |
| 13 | }; | 13 | } |
| 14 | 14 | ||
| 15 | const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { | 15 | const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { |
| 16 | |||
| 17 | const navigate = useNavigate(); | 16 | const navigate = useNavigate(); |
| 18 | 17 | ||
| 19 | const _login = () => { | 18 | const _login = () => { |
| 20 | window.location.href = "/api/v1/login"; | 19 | window.location.href = '/api/v1/login'; |
| 21 | }; | 20 | }; |
| 22 | 21 | ||
| 23 | const _logout = () => { | 22 | const _logout = () => { |
| 24 | setProfile(undefined); | 23 | setProfile(undefined); |
| 25 | setToken(undefined); | 24 | setToken(undefined); |
| 26 | API.delete_token(); | 25 | API.delete_token(); |
| 27 | navigate("/"); | 26 | navigate('/'); |
| 28 | }; | 27 | }; |
| 29 | 28 | ||
| 30 | return ( | 29 | return ( |
| 31 | <> | 30 | <> |
| 32 | {profile | 31 | {profile ? ( |
| 33 | ? | 32 | <> |
| 34 | ( | 33 | {profile.profile ? ( |
| 35 | <> | 34 | <> |
| 36 | {profile.profile ? | 35 | <Link to="/profile" tabIndex={-1} className="login"> |
| 37 | ( | 36 | <button className="sidebar-button"> |
| 38 | <> | 37 | <img |
| 39 | <Link to="/profile" tabIndex={-1} className='login'> | 38 | className="avatar-img" |
| 40 | <button className='sidebar-button'> | 39 | src={profile.avatar_link} |
| 41 | <img className="avatar-img" src={profile.avatar_link} alt="" /> | 40 | alt="" |
| 42 | <span>{profile.user_name}</span> | 41 | /> |
| 43 | </button> | 42 | <span>{profile.user_name}</span> |
| 44 | <button className='logout-button' onClick={_logout}> | 43 | </button> |
| 45 | <img src={ExitIcon} alt="" /><span /> | 44 | <button className="logout-button" onClick={_logout}> |
| 46 | </button> | 45 | <img src={ExitIcon} alt="" /> |
| 47 | </Link> | 46 | <span /> |
| 48 | </> | 47 | </button> |
| 49 | ) | 48 | </Link> |
| 50 | : | 49 | </> |
| 51 | ( | 50 | ) : ( |
| 52 | <> | 51 | <> |
| 53 | <Link to="/" tabIndex={-1} className='login'> | 52 | <Link to="/" tabIndex={-1} className="login"> |
| 54 | <button className='sidebar-button'> | 53 | <button className="sidebar-button"> |
| 55 | <img className="avatar-img" src={profile.avatar_link} alt="" /> | 54 | <img |
| 56 | <span>Loading Profile...</span> | 55 | className="avatar-img" |
| 57 | </button> | 56 | src={profile.avatar_link} |
| 58 | <button disabled className='logout-button' onClick={_logout}> | 57 | alt="" |
| 59 | <img src={ExitIcon} alt="" /><span /> | 58 | /> |
| 60 | </button> | 59 | <span>Loading Profile...</span> |
| 61 | </Link> | 60 | </button> |
| 62 | </> | 61 | <button disabled className="logout-button" onClick={_logout}> |
| 63 | ) | 62 | <img src={ExitIcon} alt="" /> |
| 64 | } | 63 | <span /> |
| 65 | </> | 64 | </button> |
| 66 | ) | 65 | </Link> |
| 67 | : | 66 | </> |
| 68 | ( | 67 | )} |
| 69 | <Link to="/api/v1/login" tabIndex={-1} className='login' > | 68 | </> |
| 70 | <button className='sidebar-button' onClick={_login}> | 69 | ) : ( |
| 71 | <img className="avatar-img" src={UserIcon} alt="" /> | 70 | <Link to="/api/v1/login" tabIndex={-1} className="login"> |
| 72 | <span> | 71 | <button className="sidebar-button" onClick={_login}> |
| 73 | <img src={LoginIcon} alt="Sign in through Steam" /> | 72 | <img className="avatar-img" src={UserIcon} alt="" /> |
| 74 | </span> | 73 | <span> |
| 75 | </button> | 74 | <img src={LoginIcon} alt="Sign in through Steam" /> |
| 76 | </Link> | 75 | </span> |
| 77 | )} | 76 | </button> |
| 77 | </Link> | ||
| 78 | )} | ||
| 78 | </> | 79 | </> |
| 79 | ); | 80 | ); |
| 80 | }; | 81 | }; |
diff --git a/frontend/src/components/MapEntry.tsx b/frontend/src/components/MapEntry.tsx index 0f494ad..f1dee5b 100644 --- a/frontend/src/components/MapEntry.tsx +++ b/frontend/src/components/MapEntry.tsx | |||
| @@ -1,12 +1,8 @@ | |||
| 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 <div></div>; |
| 6 | <div> | 6 | }; |
| 7 | |||
| 8 | </div> | ||
| 9 | ) | ||
| 10 | } | ||
| 11 | 7 | ||
| 12 | export default MapEntry; | 8 | export default MapEntry; |
diff --git a/frontend/src/components/MessageDialog.tsx b/frontend/src/components/MessageDialog.tsx index 5c85189..b739ebc 100644 --- a/frontend/src/components/MessageDialog.tsx +++ b/frontend/src/components/MessageDialog.tsx | |||
| @@ -1,29 +1,33 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | 2 | ||
| 3 | import "@css/Dialog.css" | 3 | import '@css/Dialog.css'; |
| 4 | 4 | ||
| 5 | interface MessageDialogProps { | 5 | interface MessageDialogProps { |
| 6 | title: string; | 6 | title: string; |
| 7 | subtitle: string; | 7 | subtitle: string; |
| 8 | onClose: () => void; | 8 | onClose: () => void; |
| 9 | }; | 9 | } |
| 10 | 10 | ||
| 11 | const MessageDialog: React.FC<MessageDialogProps> = ({ title, subtitle, onClose }) => { | 11 | const MessageDialog: React.FC<MessageDialogProps> = ({ |
| 12 | return ( | 12 | title, |
| 13 | <div className='dimmer'> | 13 | subtitle, |
| 14 | <div className='dialog'> | 14 | onClose, |
| 15 | <div className='dialog-element dialog-header'> | 15 | }) => { |
| 16 | <span>{title}</span> | 16 | return ( |
| 17 | </div> | 17 | <div className="dimmer"> |
| 18 | <div className='dialog-element dialog-description'> | 18 | <div className="dialog"> |
| 19 | <span>{subtitle}</span> | 19 | <div className="dialog-element dialog-header"> |
| 20 | </div> | 20 | <span>{title}</span> |
| 21 | <div className='dialog-element dialog-btns-container'> | ||
| 22 | <button onClick={onClose}>Close</button> | ||
| 23 | </div> | ||
| 24 | </div> | ||
| 25 | </div> | 21 | </div> |
| 26 | ) | 22 | <div className="dialog-element dialog-description"> |
| 27 | } | 23 | <span>{subtitle}</span> |
| 24 | </div> | ||
| 25 | <div className="dialog-element dialog-btns-container"> | ||
| 26 | <button onClick={onClose}>Close</button> | ||
| 27 | </div> | ||
| 28 | </div> | ||
| 29 | </div> | ||
| 30 | ); | ||
| 31 | }; | ||
| 28 | 32 | ||
| 29 | export default MessageDialog; | 33 | export default MessageDialog; |
diff --git a/frontend/src/components/MessageDialogLoad.tsx b/frontend/src/components/MessageDialogLoad.tsx index 966e064..acea27d 100644 --- a/frontend/src/components/MessageDialogLoad.tsx +++ b/frontend/src/components/MessageDialogLoad.tsx | |||
| @@ -1,29 +1,31 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | 2 | ||
| 3 | import "@css/Dialog.css" | 3 | import '@css/Dialog.css'; |
| 4 | 4 | ||
| 5 | interface MessageDialogLoadProps { | 5 | interface MessageDialogLoadProps { |
| 6 | title: string; | 6 | title: string; |
| 7 | onClose: () => void; | 7 | onClose: () => void; |
| 8 | }; | 8 | } |
| 9 | 9 | ||
| 10 | const MessageDialogLoad: React.FC<MessageDialogLoadProps> = ({ title, onClose }) => { | 10 | const MessageDialogLoad: React.FC<MessageDialogLoadProps> = ({ |
| 11 | return ( | 11 | title, |
| 12 | <div className='dimmer'> | 12 | onClose, |
| 13 | <div className='dialog'> | 13 | }) => { |
| 14 | <div className='dialog-element dialog-header'> | 14 | return ( |
| 15 | <span>{title}</span> | 15 | <div className="dimmer"> |
| 16 | </div> | 16 | <div className="dialog"> |
| 17 | <div className='dialog-element dialog-description'> | 17 | <div className="dialog-element dialog-header"> |
| 18 | <div style={{display: "flex", justifyContent: "center"}}> | 18 | <span>{title}</span> |
| 19 | <span className="loader"></span> | ||
| 20 | </div> | ||
| 21 | </div> | ||
| 22 | <div className='dialog-element dialog-btns-container'> | ||
| 23 | </div> | ||
| 24 | </div> | ||
| 25 | </div> | 19 | </div> |
| 26 | ) | 20 | <div className="dialog-element dialog-description"> |
| 27 | } | 21 | <div style={{ display: 'flex', justifyContent: 'center' }}> |
| 22 | <span className="loader"></span> | ||
| 23 | </div> | ||
| 24 | </div> | ||
| 25 | <div className="dialog-element dialog-btns-container"></div> | ||
| 26 | </div> | ||
| 27 | </div> | ||
| 28 | ); | ||
| 29 | }; | ||
| 28 | 30 | ||
| 29 | export default MessageDialogLoad; | 31 | export default MessageDialogLoad; |
diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx index f765cd8..140d6a3 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; |
| @@ -28,26 +28,26 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 28 | 28 | ||
| 29 | const [routeContent, setRouteContent] = React.useState<ModMenuContent>({ | 29 | const [routeContent, setRouteContent] = React.useState<ModMenuContent>({ |
| 30 | id: 0, | 30 | id: 0, |
| 31 | name: "", | 31 | name: '', |
| 32 | score: 0, | 32 | score: 0, |
| 33 | date: "", | 33 | date: '', |
| 34 | showcase: "", | 34 | showcase: '', |
| 35 | description: "No description available.", | 35 | description: 'No description available.', |
| 36 | category_id: 1, | 36 | category_id: 1, |
| 37 | }); | 37 | }); |
| 38 | 38 | ||
| 39 | const [image, setImage] = React.useState<string>(""); | 39 | const [image, setImage] = React.useState<string>(''); |
| 40 | const [md, setMd] = React.useState<string>(""); | 40 | const [md, setMd] = React.useState<string>(''); |
| 41 | 41 | ||
| 42 | const navigate = useNavigate(); | 42 | const navigate = useNavigate(); |
| 43 | 43 | ||
| 44 | function compressImage(file: File): Promise<string> { | 44 | function compressImage(file: File): Promise<string> { |
| 45 | const reader = new FileReader(); | 45 | const reader = new FileReader(); |
| 46 | reader.readAsDataURL(file); | 46 | reader.readAsDataURL(file); |
| 47 | return new Promise((resolve) => { | 47 | return new Promise(resolve => { |
| 48 | reader.onload = () => { | 48 | reader.onload = () => { |
| 49 | const img = new Image(); | 49 | const img = new Image(); |
| 50 | if (typeof reader.result === "string") { | 50 | if (typeof reader.result === 'string') { |
| 51 | img.src = reader.result; | 51 | img.src = reader.result; |
| 52 | img.onload = () => { | 52 | img.onload = () => { |
| 53 | let { width, height } = img; | 53 | let { width, height } = img; |
| @@ -59,10 +59,10 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 59 | width *= 320 / height; | 59 | width *= 320 / height; |
| 60 | height = 320; | 60 | height = 320; |
| 61 | } | 61 | } |
| 62 | const canvas = document.createElement("canvas"); | 62 | const canvas = document.createElement('canvas'); |
| 63 | canvas.width = width; | 63 | canvas.width = width; |
| 64 | canvas.height = height; | 64 | canvas.height = height; |
| 65 | canvas.getContext("2d")!.drawImage(img, 0, 0, width, height); | 65 | canvas.getContext('2d')!.drawImage(img, 0, 0, width, height); |
| 66 | resolve(canvas.toDataURL(file.type, 0.6)); | 66 | resolve(canvas.toDataURL(file.type, 0.6)); |
| 67 | }; | 67 | }; |
| 68 | } | 68 | } |
| @@ -73,8 +73,8 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 73 | const _edit_map_summary_image = async () => { | 73 | const _edit_map_summary_image = async () => { |
| 74 | if ( | 74 | if ( |
| 75 | await confirm( | 75 | await confirm( |
| 76 | "Edit Map Summary Image", | 76 | 'Edit Map Summary Image', |
| 77 | "Are you sure you want to submit this to the database?" | 77 | 'Are you sure you want to submit this to the database?' |
| 78 | ) | 78 | ) |
| 79 | ) { | 79 | ) { |
| 80 | if (token) { | 80 | if (token) { |
| @@ -82,7 +82,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 82 | if (success) { | 82 | if (success) { |
| 83 | navigate(0); | 83 | navigate(0); |
| 84 | } else { | 84 | } else { |
| 85 | alert("Error. Check logs."); | 85 | alert('Error. Check logs.'); |
| 86 | } | 86 | } |
| 87 | } | 87 | } |
| 88 | } | 88 | } |
| @@ -91,17 +91,17 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 91 | const _edit_map_summary_route = async () => { | 91 | const _edit_map_summary_route = async () => { |
| 92 | if ( | 92 | if ( |
| 93 | await confirm( | 93 | await confirm( |
| 94 | "Edit Map Summary Route", | 94 | 'Edit Map Summary Route', |
| 95 | "Are you sure you want to submit this to the database?" | 95 | 'Are you sure you want to submit this to the database?' |
| 96 | ) | 96 | ) |
| 97 | ) { | 97 | ) { |
| 98 | if (token) { | 98 | if (token) { |
| 99 | routeContent.date += "T00:00:00Z"; | 99 | routeContent.date += 'T00:00:00Z'; |
| 100 | const success = await API.put_map_summary(token, mapID, routeContent); | 100 | const success = await API.put_map_summary(token, mapID, routeContent); |
| 101 | if (success) { | 101 | if (success) { |
| 102 | navigate(0); | 102 | navigate(0); |
| 103 | } else { | 103 | } else { |
| 104 | alert("Error. Check logs."); | 104 | alert('Error. Check logs.'); |
| 105 | } | 105 | } |
| 106 | } | 106 | } |
| 107 | } | 107 | } |
| @@ -110,17 +110,17 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 110 | const _create_map_summary_route = async () => { | 110 | const _create_map_summary_route = async () => { |
| 111 | if ( | 111 | if ( |
| 112 | await confirm( | 112 | await confirm( |
| 113 | "Create Map Summary Route", | 113 | 'Create Map Summary Route', |
| 114 | "Are you sure you want to submit this to the database?" | 114 | 'Are you sure you want to submit this to the database?' |
| 115 | ) | 115 | ) |
| 116 | ) { | 116 | ) { |
| 117 | if (token) { | 117 | if (token) { |
| 118 | routeContent.date += "T00:00:00Z"; | 118 | routeContent.date += 'T00:00:00Z'; |
| 119 | const success = await API.post_map_summary(token, mapID, routeContent); | 119 | const success = await API.post_map_summary(token, mapID, routeContent); |
| 120 | if (success) { | 120 | if (success) { |
| 121 | navigate(0); | 121 | navigate(0); |
| 122 | } else { | 122 | } else { |
| 123 | alert("Error. Check logs."); | 123 | alert('Error. Check logs.'); |
| 124 | } | 124 | } |
| 125 | } | 125 | } |
| 126 | } | 126 | } |
| @@ -129,7 +129,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 129 | const _delete_map_summary_route = async () => { | 129 | const _delete_map_summary_route = async () => { |
| 130 | if ( | 130 | if ( |
| 131 | await confirm( | 131 | await confirm( |
| 132 | "Delete Map Summary Route", | 132 | 'Delete Map Summary Route', |
| 133 | `Are you sure you want to submit this to the database?\n | 133 | `Are you sure you want to submit this to the database?\n |
| 134 | ${data.summary.routes[selectedRun].category.name}\n${data.summary.routes[selectedRun].history.score_count} portals\n${data.summary.routes[selectedRun].history.runner_name}` | 134 | ${data.summary.routes[selectedRun].category.name}\n${data.summary.routes[selectedRun].history.score_count} portals\n${data.summary.routes[selectedRun].history.runner_name}` |
| 135 | ) | 135 | ) |
| @@ -143,7 +143,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 143 | if (success) { | 143 | if (success) { |
| 144 | navigate(0); | 144 | navigate(0); |
| 145 | } else { | 145 | } else { |
| 146 | alert("Error. Check logs."); | 146 | alert('Error. Check logs.'); |
| 147 | } | 147 | } |
| 148 | } | 148 | } |
| 149 | } | 149 | } |
| @@ -154,14 +154,14 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 154 | // add route | 154 | // add route |
| 155 | setRouteContent({ | 155 | setRouteContent({ |
| 156 | id: 0, | 156 | id: 0, |
| 157 | name: "", | 157 | name: '', |
| 158 | score: 0, | 158 | score: 0, |
| 159 | date: "", | 159 | date: '', |
| 160 | showcase: "", | 160 | showcase: '', |
| 161 | description: "No description available.", | 161 | description: 'No description available.', |
| 162 | category_id: 1, | 162 | category_id: 1, |
| 163 | }); | 163 | }); |
| 164 | setMd("No description available."); | 164 | setMd('No description available.'); |
| 165 | } | 165 | } |
| 166 | if (menu === 2) { | 166 | if (menu === 2) { |
| 167 | // edit route | 167 | // edit route |
| @@ -169,7 +169,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 169 | id: data.summary.routes[selectedRun].route_id, | 169 | id: data.summary.routes[selectedRun].route_id, |
| 170 | name: data.summary.routes[selectedRun].history.runner_name, | 170 | name: data.summary.routes[selectedRun].history.runner_name, |
| 171 | score: data.summary.routes[selectedRun].history.score_count, | 171 | score: data.summary.routes[selectedRun].history.score_count, |
| 172 | date: data.summary.routes[selectedRun].history.date.split("T")[0], | 172 | date: data.summary.routes[selectedRun].history.date.split('T')[0], |
| 173 | showcase: data.summary.routes[selectedRun].showcase, | 173 | showcase: data.summary.routes[selectedRun].showcase, |
| 174 | description: data.summary.routes[selectedRun].description, | 174 | description: data.summary.routes[selectedRun].description, |
| 175 | category_id: data.summary.routes[selectedRun].category.id, | 175 | category_id: data.summary.routes[selectedRun].category.id, |
| @@ -179,20 +179,20 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 179 | }, [menu, data.summary.routes, selectedRun]); | 179 | }, [menu, data.summary.routes, selectedRun]); |
| 180 | 180 | ||
| 181 | React.useEffect(() => { | 181 | React.useEffect(() => { |
| 182 | const modview = document.querySelector("div#modview") as HTMLElement; | 182 | const modview = document.querySelector('div#modview') as HTMLElement; |
| 183 | if (modview) { | 183 | if (modview) { |
| 184 | showButton | 184 | showButton |
| 185 | ? (modview.style.transform = "translateY(-68%)") | 185 | ? (modview.style.transform = 'translateY(-68%)') |
| 186 | : (modview.style.transform = "translateY(0%)"); | 186 | : (modview.style.transform = 'translateY(0%)'); |
| 187 | } | 187 | } |
| 188 | 188 | ||
| 189 | const modview_block = document.querySelector( | 189 | const modview_block = document.querySelector( |
| 190 | "#modview_block" | 190 | '#modview_block' |
| 191 | ) as HTMLElement; | 191 | ) as HTMLElement; |
| 192 | if (modview_block) { | 192 | if (modview_block) { |
| 193 | showButton | 193 | showButton |
| 194 | ? (modview_block.style.display = "none") | 194 | ? (modview_block.style.display = 'none') |
| 195 | : (modview_block.style.display = "block"); | 195 | : (modview_block.style.display = 'block'); |
| 196 | } | 196 | } |
| 197 | }, [showButton]); | 197 | }, [showButton]); |
| 198 | 198 | ||
| @@ -240,11 +240,9 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 240 | <input | 240 | <input |
| 241 | type="file" | 241 | type="file" |
| 242 | accept="image/*" | 242 | accept="image/*" |
| 243 | onChange={(e) => { | 243 | onChange={e => { |
| 244 | if (e.target.files) { | 244 | if (e.target.files) { |
| 245 | compressImage(e.target.files[0]).then((d) => | 245 | compressImage(e.target.files[0]).then(d => setImage(d)); |
| 246 | setImage(d) | ||
| 247 | ); | ||
| 248 | } | 246 | } |
| 249 | }} | 247 | }} |
| 250 | /> | 248 | /> |
| @@ -275,7 +273,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 275 | <input | 273 | <input |
| 276 | type="text" | 274 | type="text" |
| 277 | value={routeContent.name} | 275 | value={routeContent.name} |
| 278 | onChange={(e) => { | 276 | onChange={e => { |
| 279 | setRouteContent({ | 277 | setRouteContent({ |
| 280 | ...routeContent, | 278 | ...routeContent, |
| 281 | name: e.target.value, | 279 | name: e.target.value, |
| @@ -288,7 +286,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 288 | <input | 286 | <input |
| 289 | type="number" | 287 | type="number" |
| 290 | value={routeContent.score} | 288 | value={routeContent.score} |
| 291 | onChange={(e) => { | 289 | onChange={e => { |
| 292 | setRouteContent({ | 290 | setRouteContent({ |
| 293 | ...routeContent, | 291 | ...routeContent, |
| 294 | score: parseInt(e.target.value), | 292 | score: parseInt(e.target.value), |
| @@ -301,7 +299,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 301 | <input | 299 | <input |
| 302 | type="date" | 300 | type="date" |
| 303 | value={routeContent.date} | 301 | value={routeContent.date} |
| 304 | onChange={(e) => { | 302 | onChange={e => { |
| 305 | setRouteContent({ | 303 | setRouteContent({ |
| 306 | ...routeContent, | 304 | ...routeContent, |
| 307 | date: e.target.value, | 305 | date: e.target.value, |
| @@ -314,7 +312,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 314 | <input | 312 | <input |
| 315 | type="text" | 313 | type="text" |
| 316 | value={routeContent.showcase} | 314 | value={routeContent.showcase} |
| 317 | onChange={(e) => { | 315 | onChange={e => { |
| 318 | setRouteContent({ | 316 | setRouteContent({ |
| 319 | ...routeContent, | 317 | ...routeContent, |
| 320 | showcase: e.target.value, | 318 | showcase: e.target.value, |
| @@ -324,12 +322,12 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 324 | </div> | 322 | </div> |
| 325 | <div | 323 | <div |
| 326 | id="modview-route-description" | 324 | id="modview-route-description" |
| 327 | style={{ height: "180px", gridColumn: "1 / span 5" }} | 325 | style={{ height: '180px', gridColumn: '1 / span 5' }} |
| 328 | > | 326 | > |
| 329 | <span>Description:</span> | 327 | <span>Description:</span> |
| 330 | <textarea | 328 | <textarea |
| 331 | value={routeContent.description} | 329 | value={routeContent.description} |
| 332 | onChange={(e) => { | 330 | onChange={e => { |
| 333 | setRouteContent({ | 331 | setRouteContent({ |
| 334 | ...routeContent, | 332 | ...routeContent, |
| 335 | description: e.target.value, | 333 | description: e.target.value, |
| @@ -339,7 +337,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 339 | /> | 337 | /> |
| 340 | </div> | 338 | </div> |
| 341 | <button | 339 | <button |
| 342 | style={{ gridColumn: "2 / span 3", height: "40px" }} | 340 | style={{ gridColumn: '2 / span 3', height: '40px' }} |
| 343 | onClick={_edit_map_summary_route} | 341 | onClick={_edit_map_summary_route} |
| 344 | > | 342 | > |
| 345 | Apply | 343 | Apply |
| @@ -380,7 +378,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 380 | <div id="modview-route-category"> | 378 | <div id="modview-route-category"> |
| 381 | <span>Category:</span> | 379 | <span>Category:</span> |
| 382 | <select | 380 | <select |
| 383 | onChange={(e) => { | 381 | onChange={e => { |
| 384 | setRouteContent({ | 382 | setRouteContent({ |
| 385 | ...routeContent, | 383 | ...routeContent, |
| 386 | category_id: parseInt(e.target.value), | 384 | category_id: parseInt(e.target.value), |
| @@ -393,8 +391,8 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 393 | <option value="2" key="2"> | 391 | <option value="2" key="2"> |
| 394 | No SLA | 392 | No SLA |
| 395 | </option> | 393 | </option> |
| 396 | {data.map.game_name === "Portal 2 - Cooperative" ? ( | 394 | {data.map.game_name === 'Portal 2 - Cooperative' ? ( |
| 397 | "" | 395 | '' |
| 398 | ) : ( | 396 | ) : ( |
| 399 | <option value="3" key="3"> | 397 | <option value="3" key="3"> |
| 400 | Inbounds SLA | 398 | Inbounds SLA |
| @@ -410,7 +408,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 410 | <input | 408 | <input |
| 411 | type="text" | 409 | type="text" |
| 412 | value={routeContent.name} | 410 | value={routeContent.name} |
| 413 | onChange={(e) => { | 411 | onChange={e => { |
| 414 | setRouteContent({ | 412 | setRouteContent({ |
| 415 | ...routeContent, | 413 | ...routeContent, |
| 416 | name: e.target.value, | 414 | name: e.target.value, |
| @@ -423,7 +421,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 423 | <input | 421 | <input |
| 424 | type="number" | 422 | type="number" |
| 425 | value={routeContent.score} | 423 | value={routeContent.score} |
| 426 | onChange={(e) => { | 424 | onChange={e => { |
| 427 | setRouteContent({ | 425 | setRouteContent({ |
| 428 | ...routeContent, | 426 | ...routeContent, |
| 429 | score: parseInt(e.target.value), | 427 | score: parseInt(e.target.value), |
| @@ -436,7 +434,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 436 | <input | 434 | <input |
| 437 | type="date" | 435 | type="date" |
| 438 | value={routeContent.date} | 436 | value={routeContent.date} |
| 439 | onChange={(e) => { | 437 | onChange={e => { |
| 440 | setRouteContent({ | 438 | setRouteContent({ |
| 441 | ...routeContent, | 439 | ...routeContent, |
| 442 | date: e.target.value, | 440 | date: e.target.value, |
| @@ -449,7 +447,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 449 | <input | 447 | <input |
| 450 | type="text" | 448 | type="text" |
| 451 | value={routeContent.showcase} | 449 | value={routeContent.showcase} |
| 452 | onChange={(e) => { | 450 | onChange={e => { |
| 453 | setRouteContent({ | 451 | setRouteContent({ |
| 454 | ...routeContent, | 452 | ...routeContent, |
| 455 | showcase: e.target.value, | 453 | showcase: e.target.value, |
| @@ -459,12 +457,12 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 459 | </div> | 457 | </div> |
| 460 | <div | 458 | <div |
| 461 | id="modview-route-description" | 459 | id="modview-route-description" |
| 462 | style={{ height: "180px", gridColumn: "1 / span 5" }} | 460 | style={{ height: '180px', gridColumn: '1 / span 5' }} |
| 463 | > | 461 | > |
| 464 | <span>Description:</span> | 462 | <span>Description:</span> |
| 465 | <textarea | 463 | <textarea |
| 466 | value={routeContent.description} | 464 | value={routeContent.description} |
| 467 | onChange={(e) => { | 465 | onChange={e => { |
| 468 | setRouteContent({ | 466 | setRouteContent({ |
| 469 | ...routeContent, | 467 | ...routeContent, |
| 470 | description: e.target.value, | 468 | description: e.target.value, |
| @@ -474,7 +472,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 474 | /> | 472 | /> |
| 475 | </div> | 473 | </div> |
| 476 | <button | 474 | <button |
| 477 | style={{ gridColumn: "2 / span 3", height: "40px" }} | 475 | style={{ gridColumn: '2 / span 3', height: '40px' }} |
| 478 | onClick={_create_map_summary_route} | 476 | onClick={_create_map_summary_route} |
| 479 | > | 477 | > |
| 480 | Apply | 478 | Apply |
diff --git a/frontend/src/components/RankingEntry.tsx b/frontend/src/components/RankingEntry.tsx index b899965..add36ca 100644 --- a/frontend/src/components/RankingEntry.tsx +++ b/frontend/src/components/RankingEntry.tsx | |||
| @@ -1,46 +1,66 @@ | |||
| 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 { |
| 4 | RankingType, | ||
| 5 | SteamRanking, | ||
| 6 | SteamRankingType, | ||
| 7 | } from '@customTypes/Ranking'; | ||
| 4 | 8 | ||
| 5 | enum RankingCategories { | 9 | enum RankingCategories { |
| 6 | rankings_overall, | 10 | rankings_overall, |
| 7 | rankings_multiplayer, | 11 | rankings_multiplayer, |
| 8 | rankings_singleplayer | 12 | rankings_singleplayer, |
| 9 | } | 13 | } |
| 10 | 14 | ||
| 11 | interface RankingEntryProps { | 15 | interface RankingEntryProps { |
| 12 | curRankingData: RankingType | SteamRankingType; | 16 | curRankingData: RankingType | SteamRankingType; |
| 13 | currentLeaderboardType: RankingCategories | 17 | currentLeaderboardType: RankingCategories; |
| 14 | }; | ||
| 15 | |||
| 16 | const RankingEntry: React.FC<RankingEntryProps> = (prop) => { | ||
| 17 | if ("placement" in prop.curRankingData) { | ||
| 18 | return ( | ||
| 19 | <div className='leaderboard-entry'> | ||
| 20 | <span>{prop.curRankingData.placement}</span> | ||
| 21 | <div> | ||
| 22 | <Link to={`/users/${prop.curRankingData.user.steam_id}`}> | ||
| 23 | <img src={prop.curRankingData.user.avatar_link}></img> | ||
| 24 | <span>{prop.curRankingData.user.user_name}</span> | ||
| 25 | </Link> | ||
| 26 | </div> | ||
| 27 | <span>{prop.curRankingData.total_score}</span> | ||
| 28 | </div> | ||
| 29 | ) | ||
| 30 | } else { | ||
| 31 | return ( | ||
| 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> | ||
| 34 | <div> | ||
| 35 | <Link to={`/users/${prop.curRankingData.steam_id}`}> | ||
| 36 | <img src={prop.curRankingData.avatar_link}></img> | ||
| 37 | <span>{prop.curRankingData.user_name}</span> | ||
| 38 | </Link> | ||
| 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> | ||
| 41 | </div> | ||
| 42 | ) | ||
| 43 | } | ||
| 44 | } | 18 | } |
| 45 | 19 | ||
| 20 | const RankingEntry: React.FC<RankingEntryProps> = prop => { | ||
| 21 | if ('placement' in prop.curRankingData) { | ||
| 22 | return ( | ||
| 23 | <div className="leaderboard-entry"> | ||
| 24 | <span>{prop.curRankingData.placement}</span> | ||
| 25 | <div> | ||
| 26 | <Link to={`/users/${prop.curRankingData.user.steam_id}`}> | ||
| 27 | <img src={prop.curRankingData.user.avatar_link}></img> | ||
| 28 | <span>{prop.curRankingData.user.user_name}</span> | ||
| 29 | </Link> | ||
| 30 | </div> | ||
| 31 | <span>{prop.curRankingData.total_score}</span> | ||
| 32 | </div> | ||
| 33 | ); | ||
| 34 | } else { | ||
| 35 | return ( | ||
| 36 | <div className="leaderboard-entry"> | ||
| 37 | <span> | ||
| 38 | {prop.currentLeaderboardType == | ||
| 39 | RankingCategories.rankings_singleplayer | ||
| 40 | ? prop.curRankingData.sp_rank | ||
| 41 | : prop.currentLeaderboardType == | ||
| 42 | RankingCategories.rankings_multiplayer | ||
| 43 | ? prop.curRankingData.mp_rank | ||
| 44 | : prop.curRankingData.overall_rank} | ||
| 45 | </span> | ||
| 46 | <div> | ||
| 47 | <Link to={`/users/${prop.curRankingData.steam_id}`}> | ||
| 48 | <img src={prop.curRankingData.avatar_link}></img> | ||
| 49 | <span>{prop.curRankingData.user_name}</span> | ||
| 50 | </Link> | ||
| 51 | </div> | ||
| 52 | <span> | ||
| 53 | {prop.currentLeaderboardType == | ||
| 54 | RankingCategories.rankings_singleplayer | ||
| 55 | ? prop.curRankingData.sp_score | ||
| 56 | : prop.currentLeaderboardType == | ||
| 57 | RankingCategories.rankings_multiplayer | ||
| 58 | ? prop.curRankingData.mp_score | ||
| 59 | : prop.curRankingData.overall_score} | ||
| 60 | </span> | ||
| 61 | </div> | ||
| 62 | ); | ||
| 63 | } | ||
| 64 | }; | ||
| 65 | |||
| 46 | export default RankingEntry; | 66 | export default RankingEntry; |
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 67f7f3d..71b79be 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx | |||
| @@ -1,23 +1,38 @@ | |||
| 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 { |
| 5 | BookIcon, | ||
| 6 | FlagIcon, | ||
| 7 | HelpIcon, | ||
| 8 | HomeIcon, | ||
| 9 | LogoIcon, | ||
| 10 | PortalIcon, | ||
| 11 | SearchIcon, | ||
| 12 | UploadIcon, | ||
| 13 | } from '@images/Images'; | ||
| 5 | import Login from '@components/Login'; | 14 | import Login from '@components/Login'; |
| 6 | import { UserProfile } from '@customTypes/Profile'; | 15 | import { UserProfile } from '@customTypes/Profile'; |
| 7 | import { Search } from '@customTypes/Search'; | 16 | import { Search } from '@customTypes/Search'; |
| 8 | import { API } from '@api/Api'; | 17 | import { API } from '@api/Api'; |
| 9 | import "@css/Sidebar.css"; | 18 | import '@css/Sidebar.css'; |
| 10 | 19 | ||
| 11 | interface SidebarProps { | 20 | interface SidebarProps { |
| 12 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; | 21 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; |
| 13 | profile?: UserProfile; | 22 | profile?: UserProfile; |
| 14 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; | 23 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; |
| 15 | onUploadRun: () => void; | 24 | onUploadRun: () => void; |
| 16 | }; | 25 | } |
| 17 | |||
| 18 | const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUploadRun }) => { | ||
| 19 | 26 | ||
| 20 | const [searchData, setSearchData] = React.useState<Search | undefined>(undefined); | 27 | const Sidebar: React.FC<SidebarProps> = ({ |
| 28 | setToken, | ||
| 29 | profile, | ||
| 30 | setProfile, | ||
| 31 | onUploadRun, | ||
| 32 | }) => { | ||
| 33 | const [searchData, setSearchData] = React.useState<Search | undefined>( | ||
| 34 | undefined | ||
| 35 | ); | ||
| 21 | const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); | 36 | const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); |
| 22 | const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); | 37 | const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); |
| 23 | 38 | ||
| @@ -25,71 +40,86 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo | |||
| 25 | const path = location.pathname; | 40 | const path = location.pathname; |
| 26 | 41 | ||
| 27 | const handle_sidebar_click = (clicked_sidebar_idx: number) => { | 42 | const handle_sidebar_click = (clicked_sidebar_idx: number) => { |
| 28 | const btn = document.querySelectorAll("button.sidebar-button"); | 43 | const btn = document.querySelectorAll('button.sidebar-button'); |
| 29 | if (isSidebarOpen) { setSidebarOpen(false); _handle_sidebar_hide() } | 44 | if (isSidebarOpen) { |
| 45 | setSidebarOpen(false); | ||
| 46 | _handle_sidebar_hide(); | ||
| 47 | } | ||
| 30 | // clusterfuck | 48 | // clusterfuck |
| 31 | btn.forEach((e, i) => { | 49 | btn.forEach((e, i) => { |
| 32 | btn[i].classList.remove("sidebar-button-selected") | 50 | btn[i].classList.remove('sidebar-button-selected'); |
| 33 | btn[i].classList.add("sidebar-button-deselected") | 51 | btn[i].classList.add('sidebar-button-deselected'); |
| 34 | }) | 52 | }); |
| 35 | btn[clicked_sidebar_idx].classList.add("sidebar-button-selected") | 53 | btn[clicked_sidebar_idx].classList.add('sidebar-button-selected'); |
| 36 | btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected") | 54 | btn[clicked_sidebar_idx].classList.remove('sidebar-button-deselected'); |
| 37 | }; | 55 | }; |
| 38 | 56 | ||
| 39 | const _handle_sidebar_hide = () => { | 57 | const _handle_sidebar_hide = () => { |
| 40 | var btn = document.querySelectorAll("button.sidebar-button") as NodeListOf<HTMLElement> | 58 | var btn = document.querySelectorAll( |
| 41 | const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement> | 59 | 'button.sidebar-button' |
| 42 | const side = document.querySelector("#sidebar-list") as HTMLElement; | 60 | ) as NodeListOf<HTMLElement>; |
| 43 | const searchbar = document.querySelector("#searchbar") as HTMLInputElement; | 61 | const span = document.querySelectorAll( |
| 44 | const uploadRunBtn = document.querySelector("#upload-run") as HTMLInputElement; | 62 | 'button.sidebar-button>span' |
| 45 | const uploadRunSpan = document.querySelector("#upload-run>span") as HTMLInputElement; | 63 | ) as NodeListOf<HTMLElement>; |
| 64 | const side = document.querySelector('#sidebar-list') as HTMLElement; | ||
| 65 | const searchbar = document.querySelector('#searchbar') as HTMLInputElement; | ||
| 66 | const uploadRunBtn = document.querySelector( | ||
| 67 | '#upload-run' | ||
| 68 | ) as HTMLInputElement; | ||
| 69 | const uploadRunSpan = document.querySelector( | ||
| 70 | '#upload-run>span' | ||
| 71 | ) as HTMLInputElement; | ||
| 46 | 72 | ||
| 47 | if (isSidebarOpen) { | 73 | if (isSidebarOpen) { |
| 48 | if (profile) { | 74 | if (profile) { |
| 49 | const login = document.querySelectorAll(".login>button")[1] as HTMLElement; | 75 | const login = document.querySelectorAll( |
| 50 | login.style.opacity = "1" | 76 | '.login>button' |
| 51 | uploadRunBtn.style.width = "310px" | 77 | )[1] as HTMLElement; |
| 52 | uploadRunBtn.style.padding = "0.4em 0 0 11px" | 78 | login.style.opacity = '1'; |
| 53 | uploadRunSpan.style.opacity = "0" | 79 | uploadRunBtn.style.width = '310px'; |
| 80 | uploadRunBtn.style.padding = '0.4em 0 0 11px'; | ||
| 81 | uploadRunSpan.style.opacity = '0'; | ||
| 54 | setTimeout(() => { | 82 | setTimeout(() => { |
| 55 | uploadRunSpan.style.opacity = "1" | 83 | uploadRunSpan.style.opacity = '1'; |
| 56 | }, 100) | 84 | }, 100); |
| 57 | } | 85 | } |
| 58 | setSidebarOpen(false); | 86 | setSidebarOpen(false); |
| 59 | side.style.width = "320px" | 87 | side.style.width = '320px'; |
| 60 | btn.forEach((e, i) => { | 88 | btn.forEach((e, i) => { |
| 61 | e.style.width = "310px" | 89 | e.style.width = '310px'; |
| 62 | e.style.padding = "0.4em 0 0 11px" | 90 | e.style.padding = '0.4em 0 0 11px'; |
| 63 | setTimeout(() => { | 91 | setTimeout(() => { |
| 64 | span[i].style.opacity = "1" | 92 | span[i].style.opacity = '1'; |
| 65 | }, 100) | 93 | }, 100); |
| 66 | }); | 94 | }); |
| 67 | side.style.zIndex = "2" | 95 | side.style.zIndex = '2'; |
| 68 | } else { | 96 | } else { |
| 69 | if (profile) { | 97 | if (profile) { |
| 70 | const login = document.querySelectorAll(".login>button")[1] as HTMLElement; | 98 | const login = document.querySelectorAll( |
| 71 | login.style.opacity = "0" | 99 | '.login>button' |
| 72 | uploadRunBtn.style.width = "40px" | 100 | )[1] as HTMLElement; |
| 73 | uploadRunBtn.style.padding = "0.4em 0 0 5px" | 101 | login.style.opacity = '0'; |
| 74 | uploadRunSpan.style.opacity = "0" | 102 | uploadRunBtn.style.width = '40px'; |
| 103 | uploadRunBtn.style.padding = '0.4em 0 0 5px'; | ||
| 104 | uploadRunSpan.style.opacity = '0'; | ||
| 75 | } | 105 | } |
| 76 | setSidebarOpen(true); | 106 | setSidebarOpen(true); |
| 77 | side.style.width = "40px"; | 107 | side.style.width = '40px'; |
| 78 | searchbar.focus(); | 108 | searchbar.focus(); |
| 79 | btn.forEach((e, i) => { | 109 | btn.forEach((e, i) => { |
| 80 | e.style.width = "40px" | 110 | e.style.width = '40px'; |
| 81 | e.style.padding = "0.4em 0 0 5px" | 111 | e.style.padding = '0.4em 0 0 5px'; |
| 82 | span[i].style.opacity = "0" | 112 | span[i].style.opacity = '0'; |
| 83 | }) | 113 | }); |
| 84 | setTimeout(() => { | 114 | setTimeout(() => { |
| 85 | side.style.zIndex = "0" | 115 | side.style.zIndex = '0'; |
| 86 | }, 300); | 116 | }, 300); |
| 87 | } | 117 | } |
| 88 | }; | 118 | }; |
| 89 | 119 | ||
| 90 | const _handle_sidebar_lock = () => { | 120 | const _handle_sidebar_lock = () => { |
| 91 | if (!isSidebarLocked) { | 121 | if (!isSidebarLocked) { |
| 92 | _handle_sidebar_hide() | 122 | _handle_sidebar_hide(); |
| 93 | setIsSidebarLocked(true); | 123 | setIsSidebarLocked(true); |
| 94 | setTimeout(() => setIsSidebarLocked(false), 300); | 124 | setTimeout(() => setIsSidebarLocked(false), 300); |
| 95 | } | 125 | } |
| @@ -101,98 +131,148 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo | |||
| 101 | }; | 131 | }; |
| 102 | 132 | ||
| 103 | React.useEffect(() => { | 133 | React.useEffect(() => { |
| 104 | if (path === "/") { handle_sidebar_click(1) } | 134 | if (path === '/') { |
| 105 | else if (path.includes("games")) { handle_sidebar_click(2) } | 135 | handle_sidebar_click(1); |
| 106 | else if (path.includes("rankings")) { handle_sidebar_click(3) } | 136 | } else if (path.includes('games')) { |
| 137 | handle_sidebar_click(2); | ||
| 138 | } else if (path.includes('rankings')) { | ||
| 139 | handle_sidebar_click(3); | ||
| 140 | } | ||
| 107 | // else if (path.includes("news")) { handle_sidebar_click(4) } | 141 | // else if (path.includes("news")) { handle_sidebar_click(4) } |
| 108 | // else if (path.includes("scorelog")) { handle_sidebar_click(5) } | 142 | // else if (path.includes("scorelog")) { handle_sidebar_click(5) } |
| 109 | else if (path.includes("profile")) { handle_sidebar_click(4) } | 143 | else if (path.includes('profile')) { |
| 110 | else if (path.includes("rules")) { handle_sidebar_click(5) } | 144 | handle_sidebar_click(4); |
| 111 | else if (path.includes("about")) { handle_sidebar_click(6) } | 145 | } else if (path.includes('rules')) { |
| 146 | handle_sidebar_click(5); | ||
| 147 | } else if (path.includes('about')) { | ||
| 148 | handle_sidebar_click(6); | ||
| 149 | } | ||
| 112 | }, [path]); | 150 | }, [path]); |
| 113 | 151 | ||
| 114 | return ( | 152 | return ( |
| 115 | <div id='sidebar'> | 153 | <div id="sidebar"> |
| 116 | <Link to="/" tabIndex={-1}> | 154 | <Link to="/" tabIndex={-1}> |
| 117 | <div id='logo'> {/* logo */} | 155 | <div id="logo"> |
| 118 | <img src={LogoIcon} alt="" height={"80px"} /> | 156 | {' '} |
| 119 | <div id='logo-text'> | 157 | {/* logo */} |
| 120 | <span><b>PORTAL 2</b></span><br /> | 158 | <img src={LogoIcon} alt="" height={'80px'} /> |
| 159 | <div id="logo-text"> | ||
| 160 | <span> | ||
| 161 | <b>PORTAL 2</b> | ||
| 162 | </span> | ||
| 163 | <br /> | ||
| 121 | <span>Least Portals Hub</span> | 164 | <span>Least Portals Hub</span> |
| 122 | </div> | 165 | </div> |
| 123 | </div> | 166 | </div> |
| 124 | </Link> | 167 | </Link> |
| 125 | <div id='sidebar-list'> {/* List */} | 168 | <div id="sidebar-list"> |
| 126 | <div id='sidebar-toplist'> {/* Top */} | 169 | {' '} |
| 127 | 170 | {/* List */} | |
| 128 | <button className='sidebar-button' onClick={() => _handle_sidebar_lock()}><img src={SearchIcon} alt="" /><span>Search</span></button> | 171 | <div id="sidebar-toplist"> |
| 129 | 172 | {' '} | |
| 173 | {/* Top */} | ||
| 174 | <button | ||
| 175 | className="sidebar-button" | ||
| 176 | onClick={() => _handle_sidebar_lock()} | ||
| 177 | > | ||
| 178 | <img src={SearchIcon} alt="" /> | ||
| 179 | <span>Search</span> | ||
| 180 | </button> | ||
| 130 | <span></span> | 181 | <span></span> |
| 131 | |||
| 132 | <Link to="/" tabIndex={-1}> | 182 | <Link to="/" tabIndex={-1}> |
| 133 | <button className='sidebar-button'><img src={HomeIcon} alt="homepage" /><span>Home Page</span></button> | 183 | <button className="sidebar-button"> |
| 184 | <img src={HomeIcon} alt="homepage" /> | ||
| 185 | <span>Home Page</span> | ||
| 186 | </button> | ||
| 134 | </Link> | 187 | </Link> |
| 135 | |||
| 136 | <Link to="/games" tabIndex={-1}> | 188 | <Link to="/games" tabIndex={-1}> |
| 137 | <button className='sidebar-button'><img src={PortalIcon} alt="games" /><span>Games</span></button> | 189 | <button className="sidebar-button"> |
| 190 | <img src={PortalIcon} alt="games" /> | ||
| 191 | <span>Games</span> | ||
| 192 | </button> | ||
| 138 | </Link> | 193 | </Link> |
| 139 | |||
| 140 | <Link to="/rankings" tabIndex={-1}> | 194 | <Link to="/rankings" tabIndex={-1}> |
| 141 | <button className='sidebar-button'><img src={FlagIcon} alt="rankings" /><span>Rankings</span></button> | 195 | <button className="sidebar-button"> |
| 196 | <img src={FlagIcon} alt="rankings" /> | ||
| 197 | <span>Rankings</span> | ||
| 198 | </button> | ||
| 142 | </Link> | 199 | </Link> |
| 143 | |||
| 144 | {/* <Link to="/news" tabIndex={-1}> | 200 | {/* <Link to="/news" tabIndex={-1}> |
| 145 | <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button> | 201 | <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button> |
| 146 | </Link> */} | 202 | </Link> */} |
| 147 | |||
| 148 | {/* <Link to="/scorelog" tabIndex={-1}> | 203 | {/* <Link to="/scorelog" tabIndex={-1}> |
| 149 | <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score Logs</span></button> | 204 | <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score Logs</span></button> |
| 150 | </Link> */} | 205 | </Link> */} |
| 151 | </div> | 206 | </div> |
| 152 | <div id='sidebar-bottomlist'> | 207 | <div id="sidebar-bottomlist"> |
| 153 | <span></span> | 208 | <span></span> |
| 154 | 209 | ||
| 155 | { | 210 | {profile && profile.profile ? ( |
| 156 | profile && profile.profile ? | 211 | <button |
| 157 | <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Upload Record</span></button> | 212 | id="upload-run" |
| 158 | : | 213 | className="submit-run-button" |
| 159 | <span></span> | 214 | onClick={() => onUploadRun()} |
| 160 | } | 215 | > |
| 216 | <img src={UploadIcon} alt="upload" /> | ||
| 217 | <span>Upload Record</span> | ||
| 218 | </button> | ||
| 219 | ) : ( | ||
| 220 | <span></span> | ||
| 221 | )} | ||
| 161 | 222 | ||
| 162 | <Login setToken={setToken} profile={profile} setProfile={setProfile} /> | 223 | <Login |
| 224 | setToken={setToken} | ||
| 225 | profile={profile} | ||
| 226 | setProfile={setProfile} | ||
| 227 | /> | ||
| 163 | 228 | ||
| 164 | <Link to="/rules" tabIndex={-1}> | 229 | <Link to="/rules" tabIndex={-1}> |
| 165 | <button className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard Rules</span></button> | 230 | <button className="sidebar-button"> |
| 231 | <img src={BookIcon} alt="rules" /> | ||
| 232 | <span>Leaderboard Rules</span> | ||
| 233 | </button> | ||
| 166 | </Link> | 234 | </Link> |
| 167 | 235 | ||
| 168 | <Link to="/about" tabIndex={-1}> | 236 | <Link to="/about" tabIndex={-1}> |
| 169 | <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About LPHUB</span></button> | 237 | <button className="sidebar-button"> |
| 238 | <img src={HelpIcon} alt="about" /> | ||
| 239 | <span>About LPHUB</span> | ||
| 240 | </button> | ||
| 170 | </Link> | 241 | </Link> |
| 171 | </div> | 242 | </div> |
| 172 | </div> | 243 | </div> |
| 173 | <div> | 244 | <div> |
| 174 | <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} /> | 245 | <input |
| 175 | 246 | type="text" | |
| 176 | <div id='search-data'> | 247 | id="searchbar" |
| 248 | placeholder="Search for map or a player..." | ||
| 249 | onChange={e => _handle_search_change(e.target.value)} | ||
| 250 | /> | ||
| 177 | 251 | ||
| 252 | <div id="search-data"> | ||
| 178 | {searchData?.maps.map((q, index) => ( | 253 | {searchData?.maps.map((q, index) => ( |
| 179 | <Link to={`/maps/${q.id}`} className='search-map' key={index}> | 254 | <Link to={`/maps/${q.id}`} className="search-map" key={index}> |
| 180 | <span>{q.game}</span> | 255 | <span>{q.game}</span> |
| 181 | <span>{q.chapter}</span> | 256 | <span>{q.chapter}</span> |
| 182 | <span>{q.map}</span> | 257 | <span>{q.map}</span> |
| 183 | </Link> | 258 | </Link> |
| 184 | ))} | 259 | ))} |
| 185 | {searchData?.players.map((q, index) => | 260 | {searchData?.players.map((q, index) => ( |
| 186 | ( | 261 | <Link |
| 187 | <Link to={ | 262 | to={ |
| 188 | profile && q.steam_id === profile.steam_id ? `/profile` : | 263 | profile && q.steam_id === profile.steam_id |
| 189 | `/users/${q.steam_id}` | 264 | ? `/profile` |
| 190 | } className='search-player' key={index}> | 265 | : `/users/${q.steam_id}` |
| 191 | <img src={q.avatar_link} alt='pfp'></img> | 266 | } |
| 192 | <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span> | 267 | className="search-player" |
| 268 | key={index} | ||
| 269 | > | ||
| 270 | <img src={q.avatar_link} alt="pfp"></img> | ||
| 271 | <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}> | ||
| 272 | {q.user_name} | ||
| 273 | </span> | ||
| 193 | </Link> | 274 | </Link> |
| 194 | ))} | 275 | ))} |
| 195 | |||
| 196 | </div> | 276 | </div> |
| 197 | </div> | 277 | </div> |
| 198 | </div> | 278 | </div> |
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx index 4bcaa6a..1ba166a 100644 --- a/frontend/src/components/Summary.tsx +++ b/frontend/src/components/Summary.tsx | |||
| @@ -2,49 +2,67 @@ 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 { |
| 8 | selectedRun: number | 8 | selectedRun: number; |
| 9 | setSelectedRun: (x: number) => void; | 9 | setSelectedRun: (x: number) => void; |
| 10 | data: MapSummary; | 10 | data: MapSummary; |
| 11 | } | 11 | } |
| 12 | 12 | ||
| 13 | const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data }) => { | 13 | const Summary: React.FC<SummaryProps> = ({ |
| 14 | 14 | selectedRun, | |
| 15 | setSelectedRun, | ||
| 16 | data, | ||
| 17 | }) => { | ||
| 15 | const [selectedCategory, setSelectedCategory] = React.useState<number>(1); | 18 | const [selectedCategory, setSelectedCategory] = React.useState<number>(1); |
| 16 | const [historySelected, setHistorySelected] = React.useState<boolean>(false); | 19 | const [historySelected, setHistorySelected] = React.useState<boolean>(false); |
| 17 | 20 | ||
| 18 | function _select_run(idx: number, category_id: number) { | 21 | function _select_run(idx: number, category_id: number) { |
| 19 | let r = document.querySelectorAll("button.record"); | 22 | let r = document.querySelectorAll('button.record'); |
| 20 | r.forEach(e => (e as HTMLElement).style.backgroundColor = "#2b2e46"); | 23 | r.forEach(e => ((e as HTMLElement).style.backgroundColor = '#2b2e46')); |
| 21 | (r[idx] as HTMLElement).style.backgroundColor = "#161723" | 24 | (r[idx] as HTMLElement).style.backgroundColor = '#161723'; |
| 22 | |||
| 23 | 25 | ||
| 24 | if (data && data.summary.routes.length !== 0) { | 26 | if (data && data.summary.routes.length !== 0) { |
| 25 | idx += data.summary.routes.filter(e => e.category.id < category_id).length // lethimcook | 27 | idx += data.summary.routes.filter( |
| 28 | e => e.category.id < category_id | ||
| 29 | ).length; // lethimcook | ||
| 26 | setSelectedRun(idx); | 30 | setSelectedRun(idx); |
| 27 | } | 31 | } |
| 28 | }; | 32 | } |
| 29 | 33 | ||
| 30 | function _get_youtube_id(url: string): string { | 34 | function _get_youtube_id(url: string): string { |
| 31 | const urlArray = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/); | 35 | const urlArray = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/); |
| 32 | return (urlArray[2] !== undefined) ? urlArray[2].split(/[^0-9a-z_-]/i)[0] : urlArray[0]; | 36 | return urlArray[2] !== undefined |
| 33 | }; | 37 | ? urlArray[2].split(/[^0-9a-z_-]/i)[0] |
| 38 | : urlArray[0]; | ||
| 39 | } | ||
| 34 | 40 | ||
| 35 | function _category_change() { | 41 | function _category_change() { |
| 36 | const btn = document.querySelectorAll("#section3 #category span button"); | 42 | const btn = document.querySelectorAll('#section3 #category span button'); |
| 37 | btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); | 43 | btn.forEach(e => { |
| 44 | (e as HTMLElement).style.backgroundColor = '#2b2e46'; | ||
| 45 | }); | ||
| 38 | // heavenly father forgive me for i have sinned. TODO: fix this bullshit with dynamic categories | 46 | // heavenly father forgive me for i have sinned. TODO: fix this bullshit with dynamic categories |
| 39 | const idx = selectedCategory === 1 ? 0 : data.map.is_coop ? selectedCategory - 3 : selectedCategory - 1; | 47 | const idx = |
| 40 | (btn[idx] as HTMLElement).style.backgroundColor = "#202232"; | 48 | selectedCategory === 1 |
| 41 | }; | 49 | ? 0 |
| 50 | : data.map.is_coop | ||
| 51 | ? selectedCategory - 3 | ||
| 52 | : selectedCategory - 1; | ||
| 53 | (btn[idx] as HTMLElement).style.backgroundColor = '#202232'; | ||
| 54 | } | ||
| 42 | 55 | ||
| 43 | function _history_change() { | 56 | function _history_change() { |
| 44 | const btn = document.querySelectorAll("#section3 #history span button"); | 57 | const btn = document.querySelectorAll('#section3 #history span button'); |
| 45 | btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); | 58 | btn.forEach(e => { |
| 46 | (historySelected ? btn[1] as HTMLElement : btn[0] as HTMLElement).style.backgroundColor = "#202232"; | 59 | (e as HTMLElement).style.backgroundColor = '#2b2e46'; |
| 47 | }; | 60 | }); |
| 61 | (historySelected | ||
| 62 | ? (btn[1] as HTMLElement) | ||
| 63 | : (btn[0] as HTMLElement) | ||
| 64 | ).style.backgroundColor = '#202232'; | ||
| 65 | } | ||
| 48 | 66 | ||
| 49 | React.useEffect(() => { | 67 | React.useEffect(() => { |
| 50 | _history_change(); | 68 | _history_change(); |
| @@ -61,119 +79,188 @@ const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data }) | |||
| 61 | 79 | ||
| 62 | return ( | 80 | return ( |
| 63 | <> | 81 | <> |
| 64 | <section id='section3' className='summary1'> | 82 | <section id="section3" className="summary1"> |
| 65 | <div id='category' | 83 | <div |
| 66 | style={data.map.image === "" ? { backgroundColor: "#202232" } : {}}> | 84 | id="category" |
| 67 | <img src={data.map.image} alt="" id='category-image'></img> | 85 | style={data.map.image === '' ? { backgroundColor: '#202232' } : {}} |
| 68 | <p><span className='portal-count'>{data.summary.routes[selectedRun].history.score_count}</span> | 86 | > |
| 69 | {data.summary.routes[selectedRun].history.score_count === 1 ? ` portal` : ` portals`}</p> | 87 | <img src={data.map.image} alt="" id="category-image"></img> |
| 70 | {data.map.is_coop ? // TODO: make this part dynamic | 88 | <p> |
| 71 | ( | 89 | <span className="portal-count"> |
| 72 | <span style={{ gridTemplateColumns: "1fr 1fr 1fr" }}> | 90 | {data.summary.routes[selectedRun].history.score_count} |
| 73 | <button onClick={() => setSelectedCategory(1)}>CM</button> | 91 | </span> |
| 74 | <button onClick={() => setSelectedCategory(4)}>Any%</button> | 92 | {data.summary.routes[selectedRun].history.score_count === 1 |
| 75 | <button onClick={() => setSelectedCategory(5)}>All Courses</button> | 93 | ? ` portal` |
| 76 | </span> | 94 | : ` portals`} |
| 77 | ) | 95 | </p> |
| 78 | : | 96 | {data.map.is_coop ? ( // TODO: make this part dynamic |
| 79 | ( | 97 | <span style={{ gridTemplateColumns: '1fr 1fr 1fr' }}> |
| 80 | <span style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}> | 98 | <button onClick={() => setSelectedCategory(1)}>CM</button> |
| 81 | 99 | <button onClick={() => setSelectedCategory(4)}>Any%</button> | |
| 82 | <button onClick={() => setSelectedCategory(1)}>CM</button> | 100 | <button onClick={() => setSelectedCategory(5)}> |
| 83 | <button onClick={() => setSelectedCategory(2)}>NoSLA</button> | 101 | All Courses |
| 84 | <button onClick={() => setSelectedCategory(3)}>Inbounds SLA</button> | 102 | </button> |
| 85 | <button onClick={() => setSelectedCategory(4)}>Any%</button> | 103 | </span> |
| 86 | </span> | 104 | ) : ( |
| 87 | ) | 105 | <span style={{ gridTemplateColumns: '1fr 1fr 1fr 1fr' }}> |
| 88 | } | 106 | <button onClick={() => setSelectedCategory(1)}>CM</button> |
| 89 | 107 | <button onClick={() => setSelectedCategory(2)}>NoSLA</button> | |
| 108 | <button onClick={() => setSelectedCategory(3)}> | ||
| 109 | Inbounds SLA | ||
| 110 | </button> | ||
| 111 | <button onClick={() => setSelectedCategory(4)}>Any%</button> | ||
| 112 | </span> | ||
| 113 | )} | ||
| 90 | </div> | 114 | </div> |
| 91 | 115 | ||
| 92 | <div id='history'> | 116 | <div id="history"> |
| 93 | 117 | <div style={{ display: historySelected ? 'none' : 'block' }}> | |
| 94 | <div style={{ display: historySelected ? "none" : "block" }}> | 118 | {data.summary.routes.filter(e => e.category.id === selectedCategory) |
| 95 | {data.summary.routes.filter(e => e.category.id === selectedCategory).length === 0 ? <h5>There are no records for this map.</h5> : | 119 | .length === 0 ? ( |
| 120 | <h5>There are no records for this map.</h5> | ||
| 121 | ) : ( | ||
| 96 | <> | 122 | <> |
| 97 | <div className='record-top'> | 123 | <div className="record-top"> |
| 98 | <span>Date</span> | 124 | <span>Date</span> |
| 99 | <span>Record</span> | 125 | <span>Record</span> |
| 100 | <span>First Completion</span> | 126 | <span>First Completion</span> |
| 101 | </div> | 127 | </div> |
| 102 | <hr /> | 128 | <hr /> |
| 103 | <div id='records'> | 129 | <div id="records"> |
| 104 | |||
| 105 | {data.summary.routes | 130 | {data.summary.routes |
| 106 | .filter(e => e.category.id === selectedCategory) | 131 | .filter(e => e.category.id === selectedCategory) |
| 107 | .map((r, index) => ( | 132 | .map((r, index) => ( |
| 108 | <button className='record' key={index} onClick={() => { | 133 | <button |
| 109 | _select_run(index, r.category.id); | 134 | className="record" |
| 110 | }}> | 135 | key={index} |
| 111 | <span>{new Date(r.history.date).toLocaleDateString( | 136 | onClick={() => { |
| 112 | "en-US", { month: 'long', day: 'numeric', year: 'numeric' } | 137 | _select_run(index, r.category.id); |
| 113 | )}</span> | 138 | }} |
| 139 | > | ||
| 140 | <span> | ||
| 141 | {new Date(r.history.date).toLocaleDateString( | ||
| 142 | 'en-US', | ||
| 143 | { month: 'long', day: 'numeric', year: 'numeric' } | ||
| 144 | )} | ||
| 145 | </span> | ||
| 114 | <span>{r.history.score_count}</span> | 146 | <span>{r.history.score_count}</span> |
| 115 | <span>{r.history.runner_name}</span> | 147 | <span>{r.history.runner_name}</span> |
| 116 | </button> | 148 | </button> |
| 117 | ))} | 149 | ))} |
| 118 | </div> | 150 | </div> |
| 119 | </> | 151 | </> |
| 120 | } | 152 | )} |
| 121 | </div> | 153 | </div> |
| 122 | 154 | ||
| 123 | <div style={{ display: historySelected ? "block" : "none" }}> | 155 | <div style={{ display: historySelected ? 'block' : 'none' }}> |
| 124 | {data.summary.routes.filter(e => e.category.id === selectedCategory).length === 0 ? <h5>There are no records for this map.</h5> : | 156 | {data.summary.routes.filter(e => e.category.id === selectedCategory) |
| 125 | <div id='graph'> | 157 | .length === 0 ? ( |
| 158 | <h5>There are no records for this map.</h5> | ||
| 159 | ) : ( | ||
| 160 | <div id="graph"> | ||
| 126 | {/* <div>{graph(1)}</div> | 161 | {/* <div>{graph(1)}</div> |
| 127 | <div>{graph(2)}</div> | 162 | <div>{graph(2)}</div> |
| 128 | <div>{graph(3)}</div> */} | 163 | <div>{graph(3)}</div> */} |
| 129 | </div> | 164 | </div> |
| 130 | } | 165 | )} |
| 131 | </div> | 166 | </div> |
| 132 | <span> | 167 | <span> |
| 133 | <button onClick={() => setHistorySelected(false)}>List</button> | 168 | <button onClick={() => setHistorySelected(false)}>List</button> |
| 134 | <button onClick={() => setHistorySelected(true)}>Graph</button> | 169 | <button onClick={() => setHistorySelected(true)}>Graph</button> |
| 135 | </span> | 170 | </span> |
| 136 | </div> | 171 | </div> |
| 137 | 172 | </section> | |
| 138 | 173 | <section id="section4" className="summary1"> | |
| 139 | </section > | 174 | <div id="difficulty"> |
| 140 | <section id='section4' className='summary1'> | ||
| 141 | <div id='difficulty'> | ||
| 142 | <span>Difficulty</span> | 175 | <span>Difficulty</span> |
| 143 | {data.summary.routes[selectedRun].rating === 0 && (<span>N/A</span>)} | 176 | {data.summary.routes[selectedRun].rating === 0 && <span>N/A</span>} |
| 144 | {data.summary.routes[selectedRun].rating === 1 && (<span style={{ color: "lime" }}>Very easy</span>)} | 177 | {data.summary.routes[selectedRun].rating === 1 && ( |
| 145 | {data.summary.routes[selectedRun].rating === 2 && (<span style={{ color: "green" }}>Easy</span>)} | 178 | <span style={{ color: 'lime' }}>Very easy</span> |
| 146 | {data.summary.routes[selectedRun].rating === 3 && (<span style={{ color: "yellow" }}>Medium</span>)} | 179 | )} |
| 147 | {data.summary.routes[selectedRun].rating === 4 && (<span style={{ color: "orange" }}>Hard</span>)} | 180 | {data.summary.routes[selectedRun].rating === 2 && ( |
| 148 | {data.summary.routes[selectedRun].rating === 5 && (<span style={{ color: "red" }}>Very hard</span>)} | 181 | <span style={{ color: 'green' }}>Easy</span> |
| 182 | )} | ||
| 183 | {data.summary.routes[selectedRun].rating === 3 && ( | ||
| 184 | <span style={{ color: 'yellow' }}>Medium</span> | ||
| 185 | )} | ||
| 186 | {data.summary.routes[selectedRun].rating === 4 && ( | ||
| 187 | <span style={{ color: 'orange' }}>Hard</span> | ||
| 188 | )} | ||
| 189 | {data.summary.routes[selectedRun].rating === 5 && ( | ||
| 190 | <span style={{ color: 'red' }}>Very hard</span> | ||
| 191 | )} | ||
| 149 | <div> | 192 | <div> |
| 150 | {data.summary.routes[selectedRun].rating === 1 ? (<div className='difficulty-rating' style={{ backgroundColor: "lime" }}></div>) : (<div className='difficulty-rating'></div>)} | 193 | {data.summary.routes[selectedRun].rating === 1 ? ( |
| 151 | {data.summary.routes[selectedRun].rating === 2 ? (<div className='difficulty-rating' style={{ backgroundColor: "green" }}></div>) : (<div className='difficulty-rating'></div>)} | 194 | <div |
| 152 | {data.summary.routes[selectedRun].rating === 3 ? (<div className='difficulty-rating' style={{ backgroundColor: "yellow" }}></div>) : (<div className='difficulty-rating'></div>)} | 195 | className="difficulty-rating" |
| 153 | {data.summary.routes[selectedRun].rating === 4 ? (<div className='difficulty-rating' style={{ backgroundColor: "orange" }}></div>) : (<div className='difficulty-rating'></div>)} | 196 | style={{ backgroundColor: 'lime' }} |
| 154 | {data.summary.routes[selectedRun].rating === 5 ? (<div className='difficulty-rating' style={{ backgroundColor: "red" }}></div>) : (<div className='difficulty-rating'></div>)} | 197 | ></div> |
| 198 | ) : ( | ||
| 199 | <div className="difficulty-rating"></div> | ||
| 200 | )} | ||
| 201 | {data.summary.routes[selectedRun].rating === 2 ? ( | ||
| 202 | <div | ||
| 203 | className="difficulty-rating" | ||
| 204 | style={{ backgroundColor: 'green' }} | ||
| 205 | ></div> | ||
| 206 | ) : ( | ||
| 207 | <div className="difficulty-rating"></div> | ||
| 208 | )} | ||
| 209 | {data.summary.routes[selectedRun].rating === 3 ? ( | ||
| 210 | <div | ||
| 211 | className="difficulty-rating" | ||
| 212 | style={{ backgroundColor: 'yellow' }} | ||
| 213 | ></div> | ||
| 214 | ) : ( | ||
| 215 | <div className="difficulty-rating"></div> | ||
| 216 | )} | ||
| 217 | {data.summary.routes[selectedRun].rating === 4 ? ( | ||
| 218 | <div | ||
| 219 | className="difficulty-rating" | ||
| 220 | style={{ backgroundColor: 'orange' }} | ||
| 221 | ></div> | ||
| 222 | ) : ( | ||
| 223 | <div className="difficulty-rating"></div> | ||
| 224 | )} | ||
| 225 | {data.summary.routes[selectedRun].rating === 5 ? ( | ||
| 226 | <div | ||
| 227 | className="difficulty-rating" | ||
| 228 | style={{ backgroundColor: 'red' }} | ||
| 229 | ></div> | ||
| 230 | ) : ( | ||
| 231 | <div className="difficulty-rating"></div> | ||
| 232 | )} | ||
| 155 | </div> | 233 | </div> |
| 156 | </div> | 234 | </div> |
| 157 | <div id='count'> | 235 | <div id="count"> |
| 158 | <span>Completion Count</span> | 236 | <span>Completion Count</span> |
| 159 | <div>{data.summary.routes[selectedRun].completion_count}</div> | 237 | <div>{data.summary.routes[selectedRun].completion_count}</div> |
| 160 | </div> | 238 | </div> |
| 161 | </section> | 239 | </section> |
| 162 | 240 | ||
| 163 | <section id='section5' className='summary1'> | 241 | <section id="section5" className="summary1"> |
| 164 | <div id='description'> | 242 | <div id="description"> |
| 165 | {data.summary.routes[selectedRun].showcase !== "" ? | 243 | {data.summary.routes[selectedRun].showcase !== '' ? ( |
| 166 | <iframe title='Showcase video' src={"https://www.youtube.com/embed/" + _get_youtube_id(data.summary.routes[selectedRun].showcase)}> </iframe> | 244 | <iframe |
| 167 | : ""} | 245 | title="Showcase video" |
| 246 | src={ | ||
| 247 | 'https://www.youtube.com/embed/' + | ||
| 248 | _get_youtube_id(data.summary.routes[selectedRun].showcase) | ||
| 249 | } | ||
| 250 | > | ||
| 251 | {' '} | ||
| 252 | </iframe> | ||
| 253 | ) : ( | ||
| 254 | '' | ||
| 255 | )} | ||
| 168 | <h3>Route Description</h3> | 256 | <h3>Route Description</h3> |
| 169 | <span id='description-text'> | 257 | <span id="description-text"> |
| 170 | <ReactMarkdown> | 258 | <ReactMarkdown> |
| 171 | {data.summary.routes[selectedRun].description} | 259 | {data.summary.routes[selectedRun].description} |
| 172 | </ReactMarkdown> | 260 | </ReactMarkdown> |
| 173 | </span> | 261 | </span> |
| 174 | </div> | 262 | </div> |
| 175 | </section> | 263 | </section> |
| 176 | |||
| 177 | </> | 264 | </> |
| 178 | ); | 265 | ); |
| 179 | }; | 266 | }; |
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx index c02fdb8..227a564 100644 --- a/frontend/src/components/UploadRunDialog.tsx +++ b/frontend/src/components/UploadRunDialog.tsx | |||
| @@ -8,7 +8,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 { |
| @@ -18,21 +18,27 @@ interface UploadRunDialogProps { | |||
| 18 | games: Game[]; | 18 | games: Game[]; |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, games }) => { | 21 | const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ |
| 22 | 22 | token, | |
| 23 | open, | ||
| 24 | onClose, | ||
| 25 | games, | ||
| 26 | }) => { | ||
| 23 | const { message, MessageDialogComponent } = useMessage(); | 27 | const { message, MessageDialogComponent } = useMessage(); |
| 24 | const { confirm, ConfirmDialogComponent } = useConfirm(); | 28 | const { confirm, ConfirmDialogComponent } = useConfirm(); |
| 25 | const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = useMessageLoad(); | 29 | const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = |
| 30 | useMessageLoad(); | ||
| 26 | 31 | ||
| 27 | const navigate = useNavigate(); | 32 | const navigate = useNavigate(); |
| 28 | 33 | ||
| 29 | const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ | 34 | const [uploadRunContent, setUploadRunContent] = |
| 30 | host_demo: null, | 35 | React.useState<UploadRunContent>({ |
| 31 | partner_demo: null, | 36 | host_demo: null, |
| 32 | }); | 37 | partner_demo: null, |
| 38 | }); | ||
| 33 | 39 | ||
| 34 | const [selectedGameID, setSelectedGameID] = React.useState<number>(0); | 40 | const [selectedGameID, setSelectedGameID] = React.useState<number>(0); |
| 35 | const [selectedGameName, setSelectedGameName] = React.useState<string>(""); | 41 | const [selectedGameName, setSelectedGameName] = React.useState<string>(''); |
| 36 | 42 | ||
| 37 | // dropdowns | 43 | // dropdowns |
| 38 | const [dropdown1Vis, setDropdown1Vis] = React.useState<boolean>(false); | 44 | const [dropdown1Vis, setDropdown1Vis] = React.useState<boolean>(false); |
| @@ -41,7 +47,8 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 41 | const [loading, setLoading] = React.useState<boolean>(false); | 47 | const [loading, setLoading] = React.useState<boolean>(false); |
| 42 | 48 | ||
| 43 | const [dragHightlight, setDragHighlight] = React.useState<boolean>(false); | 49 | const [dragHightlight, setDragHighlight] = React.useState<boolean>(false); |
| 44 | const [dragHightlightPartner, setDragHighlightPartner] = React.useState<boolean>(false); | 50 | const [dragHightlightPartner, setDragHighlightPartner] = |
| 51 | React.useState<boolean>(false); | ||
| 45 | 52 | ||
| 46 | const fileInputRef = React.useRef<HTMLInputElement>(null); | 53 | const fileInputRef = React.useRef<HTMLInputElement>(null); |
| 47 | const fileInputRefPartner = React.useRef<HTMLInputElement>(null); | 54 | const fileInputRefPartner = React.useRef<HTMLInputElement>(null); |
| @@ -52,9 +59,12 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 52 | } else { | 59 | } else { |
| 53 | fileInputRefPartner.current?.click(); | 60 | fileInputRefPartner.current?.click(); |
| 54 | } | 61 | } |
| 55 | } | 62 | }; |
| 56 | 63 | ||
| 57 | const _handle_drag_over = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { | 64 | const _handle_drag_over = ( |
| 65 | e: React.DragEvent<HTMLDivElement>, | ||
| 66 | host: boolean | ||
| 67 | ) => { | ||
| 58 | e.preventDefault(); | 68 | e.preventDefault(); |
| 59 | e.stopPropagation(); | 69 | e.stopPropagation(); |
| 60 | if (host) { | 70 | if (host) { |
| @@ -62,9 +72,12 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 62 | } else { | 72 | } else { |
| 63 | setDragHighlightPartner(true); | 73 | setDragHighlightPartner(true); |
| 64 | } | 74 | } |
| 65 | } | 75 | }; |
| 66 | 76 | ||
| 67 | const _handle_drag_leave = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { | 77 | const _handle_drag_leave = ( |
| 78 | e: React.DragEvent<HTMLDivElement>, | ||
| 79 | host: boolean | ||
| 80 | ) => { | ||
| 68 | e.preventDefault(); | 81 | e.preventDefault(); |
| 69 | e.stopPropagation(); | 82 | e.stopPropagation(); |
| 70 | if (host) { | 83 | if (host) { |
| @@ -72,7 +85,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 72 | } else { | 85 | } else { |
| 73 | setDragHighlightPartner(false); | 86 | setDragHighlightPartner(false); |
| 74 | } | 87 | } |
| 75 | } | 88 | }; |
| 76 | 89 | ||
| 77 | const _handle_drop = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { | 90 | const _handle_drop = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { |
| 78 | e.preventDefault(); | 91 | e.preventDefault(); |
| @@ -80,7 +93,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 80 | setDragHighlight(true); | 93 | setDragHighlight(true); |
| 81 | 94 | ||
| 82 | _handle_file_change(e.dataTransfer.files, host); | 95 | _handle_file_change(e.dataTransfer.files, host); |
| 83 | } | 96 | }; |
| 84 | 97 | ||
| 85 | const _handle_dropdowns = (dropdown: number) => { | 98 | const _handle_dropdowns = (dropdown: number) => { |
| 86 | setDropdown1Vis(false); | 99 | setDropdown1Vis(false); |
| @@ -89,9 +102,9 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 89 | setDropdown1Vis(!dropdown1Vis); | 102 | setDropdown1Vis(!dropdown1Vis); |
| 90 | } else if (dropdown == 2) { | 103 | } else if (dropdown == 2) { |
| 91 | setDropdown2Vis(!dropdown2Vis); | 104 | setDropdown2Vis(!dropdown2Vis); |
| 92 | document.querySelector("#dropdown2")?.scrollTo(0, 0); | 105 | document.querySelector('#dropdown2')?.scrollTo(0, 0); |
| 93 | } | 106 | } |
| 94 | } | 107 | }; |
| 95 | 108 | ||
| 96 | const _handle_game_select = async (game_id: string, game_name: string) => { | 109 | const _handle_game_select = async (game_id: string, game_name: string) => { |
| 97 | setLoading(true); | 110 | setLoading(true); |
| @@ -120,62 +133,85 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 120 | if (token) { | 133 | if (token) { |
| 121 | if (games[selectedGameID].is_coop) { | 134 | if (games[selectedGameID].is_coop) { |
| 122 | if (uploadRunContent.host_demo === null) { | 135 | if (uploadRunContent.host_demo === null) { |
| 123 | await message("Error", "You must select a host demo to upload.") | 136 | await message('Error', 'You must select a host demo to upload.'); |
| 124 | return | 137 | return; |
| 125 | } else if (uploadRunContent.partner_demo === null) { | 138 | } else if (uploadRunContent.partner_demo === null) { |
| 126 | await message("Error", "You must select a partner demo to upload.") | 139 | await message('Error', 'You must select a partner demo to upload.'); |
| 127 | return | 140 | return; |
| 128 | } | 141 | } |
| 129 | } else { | 142 | } else { |
| 130 | if (uploadRunContent.host_demo === null) { | 143 | if (uploadRunContent.host_demo === null) { |
| 131 | await message("Error", "You must select a demo to upload.") | 144 | await message('Error', 'You must select a demo to upload.'); |
| 132 | return | 145 | return; |
| 133 | } | 146 | } |
| 134 | } | 147 | } |
| 135 | const demo = SourceDemoParser.default() | 148 | const demo = SourceDemoParser.default() |
| 136 | .setOptions({ packets: true, header: true }) | 149 | .setOptions({ packets: true, header: true }) |
| 137 | .parse(await uploadRunContent.host_demo.arrayBuffer()); | 150 | .parse(await uploadRunContent.host_demo.arrayBuffer()); |
| 138 | const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>((msg) => { | 151 | const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>(msg => { |
| 139 | return msg instanceof NetMessages.SvcUserMessage && msg.userMessage instanceof ScoreboardTempUpdate; | 152 | return ( |
| 140 | }) | 153 | msg instanceof NetMessages.SvcUserMessage && |
| 154 | msg.userMessage instanceof ScoreboardTempUpdate | ||
| 155 | ); | ||
| 156 | }); | ||
| 141 | 157 | ||
| 142 | if (!scoreboard) { | 158 | if (!scoreboard) { |
| 143 | await message("Error", "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode.") | 159 | await message( |
| 144 | return | 160 | 'Error', |
| 161 | "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode." | ||
| 162 | ); | ||
| 163 | return; | ||
| 145 | } | 164 | } |
| 146 | 165 | ||
| 147 | if (!demo.mapName || !MapNames[demo.mapName]) { | 166 | if (!demo.mapName || !MapNames[demo.mapName]) { |
| 148 | await message("Error", "Error while processing demo: Invalid map name.") | 167 | await message( |
| 149 | return | 168 | 'Error', |
| 169 | 'Error while processing demo: Invalid map name.' | ||
| 170 | ); | ||
| 171 | return; | ||
| 150 | } | 172 | } |
| 151 | 173 | ||
| 152 | if (selectedGameID === 0 && MapNames[demo.mapName] > 60) { | 174 | if (selectedGameID === 0 && MapNames[demo.mapName] > 60) { |
| 153 | await message("Error", "Error while processing demo: Invalid cooperative demo in singleplayer submission.") | 175 | await message( |
| 154 | return | 176 | 'Error', |
| 177 | 'Error while processing demo: Invalid cooperative demo in singleplayer submission.' | ||
| 178 | ); | ||
| 179 | return; | ||
| 155 | } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) { | 180 | } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) { |
| 156 | await message("Error", "Error while processing demo: Invalid singleplayer demo in cooperative submission.") | 181 | await message( |
| 157 | return | 182 | 'Error', |
| 183 | 'Error while processing demo: Invalid singleplayer demo in cooperative submission.' | ||
| 184 | ); | ||
| 185 | return; | ||
| 158 | } | 186 | } |
| 159 | 187 | ||
| 160 | const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; | 188 | const { portalScore, timeScore } = |
| 189 | scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; | ||
| 161 | 190 | ||
| 162 | const userConfirmed = await confirm("Upload Record", `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`); | 191 | const userConfirmed = await confirm( |
| 192 | 'Upload Record', | ||
| 193 | `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?` | ||
| 194 | ); | ||
| 163 | 195 | ||
| 164 | if (!userConfirmed) { | 196 | if (!userConfirmed) { |
| 165 | return; | 197 | return; |
| 166 | } | 198 | } |
| 167 | 199 | ||
| 168 | messageLoad("Uploading..."); | 200 | messageLoad('Uploading...'); |
| 169 | const [success, response] = await API.post_record(token, uploadRunContent, MapNames[demo.mapName]); | 201 | const [success, response] = await API.post_record( |
| 202 | token, | ||
| 203 | uploadRunContent, | ||
| 204 | MapNames[demo.mapName] | ||
| 205 | ); | ||
| 170 | messageLoadClose(); | 206 | messageLoadClose(); |
| 171 | await message("Upload Record", response); | 207 | await message('Upload Record', response); |
| 172 | if (success) { | 208 | if (success) { |
| 173 | setUploadRunContent({ | 209 | setUploadRunContent({ |
| 174 | host_demo: null, | 210 | host_demo: null, |
| 175 | partner_demo: null, | 211 | partner_demo: null, |
| 176 | }); | 212 | }); |
| 177 | onClose(success); | 213 | onClose(success); |
| 178 | navigate("/profile"); | 214 | navigate('/profile'); |
| 179 | } | 215 | } |
| 180 | } | 216 | } |
| 181 | }; | 217 | }; |
| @@ -184,7 +220,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 184 | if (open) { | 220 | if (open) { |
| 185 | setDragHighlightPartner(false); | 221 | setDragHighlightPartner(false); |
| 186 | setDragHighlight(false); | 222 | setDragHighlight(false); |
| 187 | _handle_game_select("1", "Portal 2 - Singleplayer"); // a different approach?. | 223 | _handle_game_select('1', 'Portal 2 - Singleplayer'); // a different approach?. |
| 188 | } | 224 | } |
| 189 | }, [open]); | 225 | }, [open]); |
| 190 | 226 | ||
| @@ -196,84 +232,191 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 196 | {MessageDialogLoadComponent} | 232 | {MessageDialogLoadComponent} |
| 197 | {ConfirmDialogComponent} | 233 | {ConfirmDialogComponent} |
| 198 | 234 | ||
| 199 | <div id='upload-run-menu'> | 235 | <div id="upload-run-menu"> |
| 200 | <div id='upload-run-menu-add'> | 236 | <div id="upload-run-menu-add"> |
| 201 | <div id='upload-run-route-category'> | 237 | <div id="upload-run-route-category"> |
| 202 | <div style={{ padding: "15px 0px" }} className='upload-run-dropdown-container upload-run-item'> | 238 | <div |
| 203 | <h3 style={{ margin: "0px 0px" }}>Select Game</h3> | 239 | style={{ padding: '15px 0px' }} |
| 204 | <div onClick={() => _handle_dropdowns(1)} style={{ display: "flex", alignItems: "center", cursor: "pointer", justifyContent: "space-between", margin: "10px 0px" }}> | 240 | className="upload-run-dropdown-container upload-run-item" |
| 205 | <div className='dropdown-cur'>{selectedGameName}</div> | 241 | > |
| 206 | <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i> | 242 | <h3 style={{ margin: '0px 0px' }}>Select Game</h3> |
| 243 | <div | ||
| 244 | onClick={() => _handle_dropdowns(1)} | ||
| 245 | style={{ | ||
| 246 | display: 'flex', | ||
| 247 | alignItems: 'center', | ||
| 248 | cursor: 'pointer', | ||
| 249 | justifyContent: 'space-between', | ||
| 250 | margin: '10px 0px', | ||
| 251 | }} | ||
| 252 | > | ||
| 253 | <div className="dropdown-cur">{selectedGameName}</div> | ||
| 254 | <i | ||
| 255 | style={{ | ||
| 256 | rotate: '-90deg', | ||
| 257 | transform: 'translate(-5px, 10px)', | ||
| 258 | }} | ||
| 259 | className="triangle" | ||
| 260 | ></i> | ||
| 207 | </div> | 261 | </div> |
| 208 | <div style={{ top: "110px" }} className={dropdown1Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> | 262 | <div |
| 209 | {games.map((game) => ( | 263 | style={{ top: '110px' }} |
| 210 | <div onClick={() => { _handle_game_select(game.id.toString(), game.name); _handle_dropdowns(1) }} key={game.id}>{game.name}</div> | 264 | className={ |
| 265 | dropdown1Vis | ||
| 266 | ? 'upload-run-dropdown' | ||
| 267 | : 'upload-run-dropdown hidden' | ||
| 268 | } | ||
| 269 | > | ||
| 270 | {games.map(game => ( | ||
| 271 | <div | ||
| 272 | onClick={() => { | ||
| 273 | _handle_game_select(game.id.toString(), game.name); | ||
| 274 | _handle_dropdowns(1); | ||
| 275 | }} | ||
| 276 | key={game.id} | ||
| 277 | > | ||
| 278 | {game.name} | ||
| 279 | </div> | ||
| 211 | ))} | 280 | ))} |
| 212 | </div> | 281 | </div> |
| 213 | </div> | 282 | </div> |
| 214 | 283 | ||
| 215 | { | 284 | {!loading && ( |
| 216 | !loading && | 285 | <> |
| 217 | ( | 286 | <div> |
| 218 | <> | 287 | <h3 style={{ margin: '10px 0px' }}>Host Demo</h3> |
| 219 | 288 | <div | |
| 220 | <div> | 289 | onClick={() => { |
| 221 | <h3 style={{ margin: "10px 0px" }}>Host Demo</h3> | 290 | _handle_file_click(true); |
| 222 | <div onClick={() => { _handle_file_click(true) }} onDragOver={(e) => { _handle_drag_over(e, true) }} onDrop={(e) => { _handle_drop(e, true) }} onDragLeave={(e) => { _handle_drag_leave(e, true) }} className={`upload-run-drag-area ${dragHightlight ? "upload-run-drag-area-highlight" : ""} ${uploadRunContent.host_demo ? "upload-run-drag-area-hidden" : ""}`}> | 291 | }} |
| 223 | <input ref={fileInputRef} type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, true)} /> | 292 | onDragOver={e => { |
| 224 | {!uploadRunContent.host_demo ? | 293 | _handle_drag_over(e, true); |
| 294 | }} | ||
| 295 | onDrop={e => { | ||
| 296 | _handle_drop(e, true); | ||
| 297 | }} | ||
| 298 | onDragLeave={e => { | ||
| 299 | _handle_drag_leave(e, true); | ||
| 300 | }} | ||
| 301 | className={`upload-run-drag-area ${dragHightlight ? 'upload-run-drag-area-highlight' : ''} ${uploadRunContent.host_demo ? 'upload-run-drag-area-hidden' : ''}`} | ||
| 302 | > | ||
| 303 | <input | ||
| 304 | ref={fileInputRef} | ||
| 305 | type="file" | ||
| 306 | name="host_demo" | ||
| 307 | id="host_demo" | ||
| 308 | accept=".dem" | ||
| 309 | onChange={e => | ||
| 310 | _handle_file_change(e.target.files, true) | ||
| 311 | } | ||
| 312 | /> | ||
| 313 | {!uploadRunContent.host_demo ? ( | ||
| 314 | <div> | ||
| 315 | <span>Drag and drop</span> | ||
| 225 | <div> | 316 | <div> |
| 226 | <span>Drag and drop</span> | 317 | <span |
| 227 | <div> | 318 | style={{ |
| 228 | <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> | 319 | fontFamily: 'BarlowSemiCondensed-Regular', |
| 229 | <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> | 320 | }} |
| 230 | </div> | 321 | > |
| 322 | Or click here | ||
| 323 | </span> | ||
| 324 | <br /> | ||
| 325 | <button | ||
| 326 | style={{ | ||
| 327 | borderRadius: '24px', | ||
| 328 | padding: '5px 8px', | ||
| 329 | margin: '5px 0px', | ||
| 330 | }} | ||
| 331 | > | ||
| 332 | Upload | ||
| 333 | </button> | ||
| 231 | </div> | 334 | </div> |
| 232 | : null} | 335 | </div> |
| 233 | 336 | ) : null} | |
| 234 | <span className="upload-run-demo-name">{uploadRunContent.host_demo?.name}</span> | ||
| 235 | </div> | ||
| 236 | { | ||
| 237 | games[selectedGameID].is_coop && | ||
| 238 | ( | ||
| 239 | <> | ||
| 240 | <div> | ||
| 241 | <h3 style={{ margin: "10px 0px" }}>Partner Demo</h3> | ||
| 242 | <div onClick={() => { _handle_file_click(false) }} onDragOver={(e) => { _handle_drag_over(e, false) }} onDrop={(e) => { _handle_drop(e, false) }} onDragLeave={(e) => { _handle_drag_leave(e, false) }} className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}> | ||
| 243 | <input ref={fileInputRefPartner} type="file" name="partner_demo" id="partner_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, false)} /> {!uploadRunContent.partner_demo ? | ||
| 244 | <div> | ||
| 245 | <span>Drag and drop</span> | ||
| 246 | <div> | ||
| 247 | <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> | ||
| 248 | <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> | ||
| 249 | </div> | ||
| 250 | </div> | ||
| 251 | : null} | ||
| 252 | |||
| 253 | <span className="upload-run-demo-name">{uploadRunContent.partner_demo?.name}</span> | ||
| 254 | </div> | ||
| 255 | </div> | ||
| 256 | </> | ||
| 257 | ) | ||
| 258 | } | ||
| 259 | </div> | ||
| 260 | <div className='search-container'> | ||
| 261 | 337 | ||
| 338 | <span className="upload-run-demo-name"> | ||
| 339 | {uploadRunContent.host_demo?.name} | ||
| 340 | </span> | ||
| 262 | </div> | 341 | </div> |
| 263 | 342 | {games[selectedGameID].is_coop && ( | |
| 264 | </> | 343 | <> |
| 265 | ) | 344 | <div> |
| 266 | } | 345 | <h3 style={{ margin: '10px 0px' }}>Partner Demo</h3> |
| 346 | <div | ||
| 347 | onClick={() => { | ||
| 348 | _handle_file_click(false); | ||
| 349 | }} | ||
| 350 | onDragOver={e => { | ||
| 351 | _handle_drag_over(e, false); | ||
| 352 | }} | ||
| 353 | onDrop={e => { | ||
| 354 | _handle_drop(e, false); | ||
| 355 | }} | ||
| 356 | onDragLeave={e => { | ||
| 357 | _handle_drag_leave(e, false); | ||
| 358 | }} | ||
| 359 | className={`upload-run-drag-area ${dragHightlightPartner ? 'upload-run-drag-area-highlight-partner' : ''} ${uploadRunContent.partner_demo ? 'upload-run-drag-area-hidden' : ''}`} | ||
| 360 | > | ||
| 361 | <input | ||
| 362 | ref={fileInputRefPartner} | ||
| 363 | type="file" | ||
| 364 | name="partner_demo" | ||
| 365 | id="partner_demo" | ||
| 366 | accept=".dem" | ||
| 367 | onChange={e => | ||
| 368 | _handle_file_change(e.target.files, false) | ||
| 369 | } | ||
| 370 | />{' '} | ||
| 371 | {!uploadRunContent.partner_demo ? ( | ||
| 372 | <div> | ||
| 373 | <span>Drag and drop</span> | ||
| 374 | <div> | ||
| 375 | <span | ||
| 376 | style={{ | ||
| 377 | fontFamily: 'BarlowSemiCondensed-Regular', | ||
| 378 | }} | ||
| 379 | > | ||
| 380 | Or click here | ||
| 381 | </span> | ||
| 382 | <br /> | ||
| 383 | <button | ||
| 384 | style={{ | ||
| 385 | borderRadius: '24px', | ||
| 386 | padding: '5px 8px', | ||
| 387 | margin: '5px 0px', | ||
| 388 | }} | ||
| 389 | > | ||
| 390 | Upload | ||
| 391 | </button> | ||
| 392 | </div> | ||
| 393 | </div> | ||
| 394 | ) : null} | ||
| 395 | <span className="upload-run-demo-name"> | ||
| 396 | {uploadRunContent.partner_demo?.name} | ||
| 397 | </span> | ||
| 398 | </div> | ||
| 399 | </div> | ||
| 400 | </> | ||
| 401 | )} | ||
| 402 | </div> | ||
| 403 | <div className="search-container"></div> | ||
| 404 | </> | ||
| 405 | )} | ||
| 267 | </div> | 406 | </div> |
| 268 | <div className='upload-run-buttons-container'> | 407 | <div className="upload-run-buttons-container"> |
| 269 | <button onClick={_upload_run}>Submit</button> | 408 | <button onClick={_upload_run}>Submit</button> |
| 270 | <button onClick={() => { | 409 | <button |
| 271 | onClose(false); | 410 | onClick={() => { |
| 272 | setUploadRunContent({ | 411 | onClose(false); |
| 273 | host_demo: null, | 412 | setUploadRunContent({ |
| 274 | partner_demo: null, | 413 | host_demo: null, |
| 275 | }); | 414 | partner_demo: null, |
| 276 | }}>Cancel</button> | 415 | }); |
| 416 | }} | ||
| 417 | > | ||
| 418 | Cancel | ||
| 419 | </button> | ||
| 277 | </div> | 420 | </div> |
| 278 | </div> | 421 | </div> |
| 279 | </div> | 422 | </div> |
| @@ -281,10 +424,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 281 | ); | 424 | ); |
| 282 | } | 425 | } |
| 283 | 426 | ||
| 284 | return ( | 427 | return <></>; |
| 285 | <></> | ||
| 286 | ); | ||
| 287 | |||
| 288 | }; | 428 | }; |
| 289 | 429 | ||
| 290 | export default UploadRunDialog; | 430 | export default UploadRunDialog; |
diff --git a/frontend/src/hooks/UseConfirm.tsx b/frontend/src/hooks/UseConfirm.tsx index e86d70d..7b4bffa 100644 --- a/frontend/src/hooks/UseConfirm.tsx +++ b/frontend/src/hooks/UseConfirm.tsx | |||
| @@ -2,39 +2,46 @@ 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< |
| 9 | ((value: boolean) => void) | null | ||
| 10 | >(null); | ||
| 9 | 11 | ||
| 10 | const confirm = ( titleN: string, subtitleN: string ) => { | 12 | const confirm = (titleN: string, subtitleN: string) => { |
| 11 | setIsOpen(true); | 13 | setIsOpen(true); |
| 12 | setTitle(titleN); | 14 | setTitle(titleN); |
| 13 | setSubtitle(subtitleN); | 15 | setSubtitle(subtitleN); |
| 14 | return new Promise<boolean>((resolve) => { | 16 | return new Promise<boolean>(resolve => { |
| 15 | setResolvePromise(() => resolve); | 17 | setResolvePromise(() => resolve); |
| 16 | }); | 18 | }); |
| 17 | }; | 19 | }; |
| 18 | 20 | ||
| 19 | const handleConfirm = () => { | 21 | const handleConfirm = () => { |
| 20 | setIsOpen(false); | 22 | setIsOpen(false); |
| 21 | if (resolvePromise) { | 23 | if (resolvePromise) { |
| 22 | resolvePromise(true); | 24 | resolvePromise(true); |
| 23 | } | ||
| 24 | } | 25 | } |
| 26 | }; | ||
| 25 | 27 | ||
| 26 | const handleCancel = () => { | 28 | const handleCancel = () => { |
| 27 | setIsOpen(false); | 29 | setIsOpen(false); |
| 28 | if (resolvePromise) { | 30 | if (resolvePromise) { |
| 29 | resolvePromise(false); | 31 | resolvePromise(false); |
| 30 | } | ||
| 31 | } | 32 | } |
| 33 | }; | ||
| 32 | 34 | ||
| 33 | const ConfirmDialogComponent = isOpen && ( | 35 | const ConfirmDialogComponent = isOpen && ( |
| 34 | <ConfirmDialog title={title} subtitle={subtitle} onConfirm={handleConfirm} onCancel={handleCancel}></ConfirmDialog> | 36 | <ConfirmDialog |
| 35 | ); | 37 | title={title} |
| 38 | subtitle={subtitle} | ||
| 39 | onConfirm={handleConfirm} | ||
| 40 | onCancel={handleCancel} | ||
| 41 | ></ConfirmDialog> | ||
| 42 | ); | ||
| 36 | 43 | ||
| 37 | return { confirm, ConfirmDialogComponent }; | 44 | return { confirm, ConfirmDialogComponent }; |
| 38 | } | 45 | }; |
| 39 | 46 | ||
| 40 | export default useConfirm; | 47 | export default useConfirm; |
diff --git a/frontend/src/hooks/UseMessage.tsx b/frontend/src/hooks/UseMessage.tsx index 97ec746..e0afa59 100644 --- a/frontend/src/hooks/UseMessage.tsx +++ b/frontend/src/hooks/UseMessage.tsx | |||
| @@ -1,37 +1,43 @@ | |||
| 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>( |
| 10 | null | ||
| 11 | ); | ||
| 10 | 12 | ||
| 11 | const message = (title: string, subtitle: string) => { | 13 | const message = (title: string, subtitle: string) => { |
| 12 | setIsOpen(true); | 14 | setIsOpen(true); |
| 13 | setTitle(title); | 15 | setTitle(title); |
| 14 | setSubtitle(subtitle); | 16 | setSubtitle(subtitle); |
| 15 | return new Promise((resolve) => { | 17 | return new Promise(resolve => { |
| 16 | setResolvePromise(() => resolve); | 18 | setResolvePromise(() => resolve); |
| 17 | }); | 19 | }); |
| 18 | }; | 20 | }; |
| 19 | 21 | ||
| 20 | const handleClose = () => { | 22 | const handleClose = () => { |
| 21 | setIsOpen(false); | 23 | setIsOpen(false); |
| 22 | if (resolvePromise) { | 24 | if (resolvePromise) { |
| 23 | resolvePromise(); | 25 | resolvePromise(); |
| 24 | setResolvePromise(null); | 26 | setResolvePromise(null); |
| 25 | } | 27 | } |
| 26 | }; | 28 | }; |
| 27 | 29 | ||
| 28 | const MessageDialogComponent = isOpen && ( | 30 | const MessageDialogComponent = isOpen && ( |
| 29 | <div className="dialog-container"> | 31 | <div className="dialog-container"> |
| 30 | <MessageDialog title={title} subtitle={subtitle} onClose={handleClose}></MessageDialog> | 32 | <MessageDialog |
| 31 | </div> | 33 | title={title} |
| 32 | ); | 34 | subtitle={subtitle} |
| 35 | onClose={handleClose} | ||
| 36 | ></MessageDialog> | ||
| 37 | </div> | ||
| 38 | ); | ||
| 33 | 39 | ||
| 34 | return { message, MessageDialogComponent }; | 40 | return { message, MessageDialogComponent }; |
| 35 | } | 41 | }; |
| 36 | 42 | ||
| 37 | export default useMessage; | 43 | export default useMessage; |
diff --git a/frontend/src/hooks/UseMessageLoad.tsx b/frontend/src/hooks/UseMessageLoad.tsx index 228c2b4..ea0b5d8 100644 --- a/frontend/src/hooks/UseMessageLoad.tsx +++ b/frontend/src/hooks/UseMessageLoad.tsx | |||
| @@ -1,35 +1,40 @@ | |||
| 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>( |
| 9 | null | ||
| 10 | ); | ||
| 9 | 11 | ||
| 10 | const messageLoad = (title: string) => { | 12 | const messageLoad = (title: string) => { |
| 11 | setIsOpen(true); | 13 | setIsOpen(true); |
| 12 | setTitle(title); | 14 | setTitle(title); |
| 13 | return new Promise((resolve) => { | 15 | return new Promise(resolve => { |
| 14 | setResolvePromise(() => resolve); | 16 | setResolvePromise(() => resolve); |
| 15 | }); | 17 | }); |
| 16 | }; | 18 | }; |
| 17 | 19 | ||
| 18 | const messageLoadClose = () => { | 20 | const messageLoadClose = () => { |
| 19 | setIsOpen(false); | 21 | setIsOpen(false); |
| 20 | if (resolvePromise) { | 22 | if (resolvePromise) { |
| 21 | resolvePromise(); | 23 | resolvePromise(); |
| 22 | setResolvePromise(null); | 24 | setResolvePromise(null); |
| 23 | } | 25 | } |
| 24 | }; | 26 | }; |
| 25 | 27 | ||
| 26 | const MessageDialogLoadComponent = isOpen && ( | 28 | const MessageDialogLoadComponent = isOpen && ( |
| 27 | <div className="dialog-container"> | 29 | <div className="dialog-container"> |
| 28 | <MessageDialogLoad title={title} onClose={messageLoadClose}></MessageDialogLoad> | 30 | <MessageDialogLoad |
| 29 | </div> | 31 | title={title} |
| 30 | ); | 32 | onClose={messageLoadClose} |
| 33 | ></MessageDialogLoad> | ||
| 34 | </div> | ||
| 35 | ); | ||
| 31 | 36 | ||
| 32 | return { messageLoad, messageLoadClose, MessageDialogLoadComponent }; | 37 | return { messageLoad, messageLoadClose, MessageDialogLoadComponent }; |
| 33 | } | 38 | }; |
| 34 | 39 | ||
| 35 | export default useMessageLoad; | 40 | export default useMessageLoad; |
diff --git a/frontend/src/images/Images.tsx b/frontend/src/images/Images.tsx index 198431b..9972662 100644 --- a/frontend/src/images/Images.tsx +++ b/frontend/src/images/Images.tsx | |||
| @@ -1,5 +1,5 @@ | |||
| 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'; |
| @@ -20,7 +20,7 @@ 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; |
| 26 | export const LoginIcon = login; | 26 | export const LoginIcon = login; |
| @@ -45,4 +45,4 @@ export const SteamIcon = img17; | |||
| 45 | export const HistoryIcon = img18; | 45 | export const HistoryIcon = img18; |
| 46 | export const SortIcon = img19; | 46 | export const SortIcon = img19; |
| 47 | export const UploadIcon = img20; | 47 | export const UploadIcon = img20; |
| 48 | export const DeleteIcon = img21; \ No newline at end of file | 48 | export const DeleteIcon = img21; |
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index eec2ff4..2eef1bc 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx | |||
| @@ -1,6 +1,6 @@ | |||
| 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 | ||
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx index a8b7826..a5d34f6 100644 --- a/frontend/src/pages/About.tsx +++ b/frontend/src/pages/About.tsx | |||
| @@ -5,36 +5,34 @@ import { Helmet } from 'react-helmet'; | |||
| 5 | import '@css/About.css'; | 5 | import '@css/About.css'; |
| 6 | 6 | ||
| 7 | const About: React.FC = () => { | 7 | const About: React.FC = () => { |
| 8 | const [aboutText, setAboutText] = React.useState<string>(''); | ||
| 8 | 9 | ||
| 9 | const [aboutText, setAboutText] = React.useState<string>(""); | 10 | React.useEffect(() => { |
| 11 | const fetchReadme = async () => { | ||
| 12 | try { | ||
| 13 | const response = await fetch( | ||
| 14 | 'https://raw.githubusercontent.com/pektezol/lphub/main/README.md' | ||
| 15 | ); | ||
| 16 | if (!response.ok) { | ||
| 17 | throw new Error('Failed to fetch README'); | ||
| 18 | } | ||
| 19 | const readmeText = await response.text(); | ||
| 20 | setAboutText(readmeText); | ||
| 21 | } catch (error) { | ||
| 22 | console.error('Error fetching README:', error); | ||
| 23 | } | ||
| 24 | }; | ||
| 25 | fetchReadme(); | ||
| 26 | }, []); | ||
| 10 | 27 | ||
| 11 | React.useEffect(() => { | 28 | return ( |
| 12 | const fetchReadme = async () => { | 29 | <div id="about"> |
| 13 | try { | 30 | <Helmet> |
| 14 | const response = await fetch( | 31 | <title>LPHUB | About</title> |
| 15 | 'https://raw.githubusercontent.com/pektezol/lphub/main/README.md' | 32 | </Helmet> |
| 16 | ); | 33 | <ReactMarkdown>{aboutText}</ReactMarkdown> |
| 17 | if (!response.ok) { | 34 | </div> |
| 18 | throw new Error('Failed to fetch README'); | 35 | ); |
| 19 | } | ||
| 20 | const readmeText = await response.text(); | ||
| 21 | setAboutText(readmeText); | ||
| 22 | } catch (error) { | ||
| 23 | console.error('Error fetching README:', error); | ||
| 24 | } | ||
| 25 | }; | ||
| 26 | fetchReadme(); | ||
| 27 | }, []); | ||
| 28 | |||
| 29 | |||
| 30 | return ( | ||
| 31 | <div id="about"> | ||
| 32 | <Helmet> | ||
| 33 | <title>LPHUB | About</title> | ||
| 34 | </Helmet> | ||
| 35 | <ReactMarkdown>{aboutText}</ReactMarkdown> | ||
| 36 | </div> | ||
| 37 | ); | ||
| 38 | }; | 36 | }; |
| 39 | 37 | ||
| 40 | export default About; | 38 | export default About; |
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx index 15cc891..ae0a2d6 100644 --- a/frontend/src/pages/Games.tsx +++ b/frontend/src/pages/Games.tsx | |||
| @@ -3,44 +3,45 @@ 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 | const _page_load = () => { | ||
| 14 | const loaders = document.querySelectorAll('.loader'); | ||
| 15 | loaders.forEach(loader => { | ||
| 16 | (loader as HTMLElement).style.display = 'none'; | ||
| 17 | }); | ||
| 18 | }; | ||
| 13 | 19 | ||
| 14 | const _page_load = () => { | 20 | React.useEffect(() => { |
| 15 | const loaders = document.querySelectorAll(".loader"); | 21 | document |
| 16 | loaders.forEach((loader) => { | 22 | .querySelectorAll('.games-page-item-body') |
| 17 | (loader as HTMLElement).style.display = "none"; | 23 | .forEach((game, index) => { |
| 18 | }); | 24 | game.innerHTML = ''; |
| 19 | } | 25 | }); |
| 26 | _page_load(); | ||
| 27 | }, []); | ||
| 20 | 28 | ||
| 21 | React.useEffect(() => { | 29 | return ( |
| 22 | document.querySelectorAll(".games-page-item-body").forEach((game, index) => { | 30 | <div className="games-page"> |
| 23 | game.innerHTML = ""; | 31 | <Helmet> |
| 24 | }); | 32 | <title>LPHUB | Games</title> |
| 25 | _page_load(); | 33 | </Helmet> |
| 26 | }, []); | 34 | <section> |
| 27 | 35 | <div className="games-page-content"> | |
| 28 | return ( | 36 | <div className="games-page-item-content"> |
| 29 | <div className='games-page'> | 37 | {games.map((game, index) => ( |
| 30 | <Helmet> | 38 | <GameEntry game={game} key={index} /> |
| 31 | <title>LPHUB | Games</title> | 39 | ))} |
| 32 | </Helmet> | 40 | </div> |
| 33 | <section> | ||
| 34 | <div className='games-page-content'> | ||
| 35 | <div className='games-page-item-content'> | ||
| 36 | {games.map((game, index) => ( | ||
| 37 | <GameEntry game={game} key={index} /> | ||
| 38 | ))} | ||
| 39 | </div> | ||
| 40 | </div> | ||
| 41 | </section> | ||
| 42 | </div> | 41 | </div> |
| 43 | ); | 42 | </section> |
| 43 | </div> | ||
| 44 | ); | ||
| 44 | }; | 45 | }; |
| 45 | 46 | ||
| 46 | export default Games; | 47 | export default Games; |
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx index 4f46af5..859af52 100644 --- a/frontend/src/pages/Homepage.tsx +++ b/frontend/src/pages/Homepage.tsx | |||
| @@ -2,21 +2,30 @@ 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 | return ( | |
| 6 | return ( | 6 | <main> |
| 7 | <main> | 7 | <Helmet> |
| 8 | <Helmet> | 8 | <title>LPHUB | Homepage</title> |
| 9 | <title>LPHUB | Homepage</title> | 9 | </Helmet> |
| 10 | </Helmet> | 10 | <section> |
| 11 | <section> | 11 | <p /> |
| 12 | <p /> | 12 | <h1>Welcome to Least Portals Hub!</h1> |
| 13 | <h1>Welcome to Least Portals Hub!</h1> | 13 | <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> | 14 | At the moment, LPHUB is in beta state. This means that the site has |
| 15 | <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p> | 15 | only the core functionalities enabled for providing both collaborative |
| 16 | <p>By using LPHUB, you agree that you have read the 'Leaderboard Rules' and the 'About LPHUB' pages.</p> | 16 | information and competitive leaderboards. |
| 17 | </section> | 17 | </p> |
| 18 | </main> | 18 | <p> |
| 19 | ); | 19 | The website should feel intuitive to navigate around. For any type of |
| 20 | feedback, reach us at LPHUB Discord server. | ||
| 21 | </p> | ||
| 22 | <p> | ||
| 23 | By using LPHUB, you agree that you have read the 'Leaderboard Rules' | ||
| 24 | and the 'About LPHUB' pages. | ||
| 25 | </p> | ||
| 26 | </section> | ||
| 27 | </main> | ||
| 28 | ); | ||
| 20 | }; | 29 | }; |
| 21 | 30 | ||
| 22 | export default Homepage; | 31 | export default Homepage; |
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx index 04938cf..5138964 100644 --- a/frontend/src/pages/Maplist.tsx +++ b/frontend/src/pages/Maplist.tsx | |||
| @@ -1,11 +1,11 @@ | |||
| 1 | import React, { useEffect } from "react"; | 1 | import React, { useEffect } from 'react'; |
| 2 | import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; | 2 | import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; |
| 3 | import { Helmet } from "react-helmet"; | 3 | import { Helmet } from 'react-helmet'; |
| 4 | 4 | ||
| 5 | import "@css/Maplist.css"; | 5 | import '@css/Maplist.css'; |
| 6 | import { API } from "@api/Api"; | 6 | import { API } from '@api/Api'; |
| 7 | import { Game } from "@customTypes/Game"; | 7 | import { Game } from '@customTypes/Game'; |
| 8 | import { GameChapter, GamesChapters } from "@customTypes/Chapters"; | 8 | import { GameChapter, GamesChapters } from '@customTypes/Chapters'; |
| 9 | 9 | ||
| 10 | const Maplist: React.FC = () => { | 10 | const Maplist: React.FC = () => { |
| 11 | const [game, setGame] = React.useState<Game | null>(null); | 11 | const [game, setGame] = React.useState<Game | null>(null); |
| @@ -19,41 +19,41 @@ const Maplist: React.FC = () => { | |||
| 19 | const [curChapter, setCurChapter] = React.useState<GameChapter>(); | 19 | const [curChapter, setCurChapter] = React.useState<GameChapter>(); |
| 20 | const [numChapters, setNumChapters] = React.useState<number>(0); | 20 | const [numChapters, setNumChapters] = React.useState<number>(0); |
| 21 | 21 | ||
| 22 | const [dropdownActive, setDropdownActive] = React.useState("none"); | 22 | const [dropdownActive, setDropdownActive] = React.useState('none'); |
| 23 | 23 | ||
| 24 | const params = useParams<{ id: string, chapter: string }>(); | 24 | const params = useParams<{ id: string; chapter: string }>(); |
| 25 | const location = useLocation(); | 25 | const location = useLocation(); |
| 26 | const navigate = useNavigate(); | 26 | const navigate = useNavigate(); |
| 27 | 27 | ||
| 28 | function _update_currently_selected(catNum2: number) { | 28 | function _update_currently_selected(catNum2: number) { |
| 29 | setCurrentlySelected(catNum2); | 29 | setCurrentlySelected(catNum2); |
| 30 | navigate("/games/" + game?.id + "?cat=" + catNum2); | 30 | navigate('/games/' + game?.id + '?cat=' + catNum2); |
| 31 | setHasClicked(true); | 31 | setHasClicked(true); |
| 32 | } | 32 | } |
| 33 | 33 | ||
| 34 | const _fetch_chapters = async (chapter_id: string) => { | 34 | const _fetch_chapters = async (chapter_id: string) => { |
| 35 | const chapters = await API.get_chapters(chapter_id); | 35 | const chapters = await API.get_chapters(chapter_id); |
| 36 | setCurChapter(chapters); | 36 | setCurChapter(chapters); |
| 37 | } | 37 | }; |
| 38 | 38 | ||
| 39 | const _handle_dropdown_click = () => { | 39 | const _handle_dropdown_click = () => { |
| 40 | if (dropdownActive == "none") { | 40 | if (dropdownActive == 'none') { |
| 41 | setDropdownActive("block"); | 41 | setDropdownActive('block'); |
| 42 | } else { | 42 | } else { |
| 43 | setDropdownActive("none"); | 43 | setDropdownActive('none'); |
| 44 | } | 44 | } |
| 45 | } | 45 | }; |
| 46 | 46 | ||
| 47 | // im sorry but im too lazy to fix this right now | 47 | // im sorry but im too lazy to fix this right now |
| 48 | useEffect(() => { | 48 | useEffect(() => { |
| 49 | // gameID | 49 | // gameID |
| 50 | const gameId = parseFloat(params.id || ""); | 50 | const gameId = parseFloat(params.id || ''); |
| 51 | setId(gameId); | 51 | setId(gameId); |
| 52 | 52 | ||
| 53 | // location query params | 53 | // location query params |
| 54 | const queryParams = new URLSearchParams(location.search); | 54 | const queryParams = new URLSearchParams(location.search); |
| 55 | if (queryParams.get("chapter")) { | 55 | if (queryParams.get('chapter')) { |
| 56 | let cat = parseFloat(queryParams.get("chapter") || ""); | 56 | let cat = parseFloat(queryParams.get('chapter') || ''); |
| 57 | if (gameId == 2) { | 57 | if (gameId == 2) { |
| 58 | cat += 10; | 58 | cat += 10; |
| 59 | } | 59 | } |
| @@ -62,7 +62,7 @@ const Maplist: React.FC = () => { | |||
| 62 | 62 | ||
| 63 | const _fetch_game = async () => { | 63 | const _fetch_game = async () => { |
| 64 | const games = await API.get_games(); | 64 | const games = await API.get_games(); |
| 65 | const foundGame = games.find((game) => game.id === gameId); | 65 | const foundGame = games.find(game => game.id === gameId); |
| 66 | // console.log(foundGame) | 66 | // console.log(foundGame) |
| 67 | if (foundGame) { | 67 | if (foundGame) { |
| 68 | setGame(foundGame); | 68 | setGame(foundGame); |
| @@ -74,7 +74,7 @@ const Maplist: React.FC = () => { | |||
| 74 | const games_chapters = await API.get_games_chapters(gameId.toString()); | 74 | const games_chapters = await API.get_games_chapters(gameId.toString()); |
| 75 | setGameChapters(games_chapters); | 75 | setGameChapters(games_chapters); |
| 76 | setNumChapters(games_chapters.chapters.length); | 76 | setNumChapters(games_chapters.chapters.length); |
| 77 | } | 77 | }; |
| 78 | 78 | ||
| 79 | setLoad(true); | 79 | setLoad(true); |
| 80 | _fetch_game(); | 80 | _fetch_game(); |
| @@ -83,21 +83,19 @@ const Maplist: React.FC = () => { | |||
| 83 | 83 | ||
| 84 | useEffect(() => { | 84 | useEffect(() => { |
| 85 | const queryParams = new URLSearchParams(location.search); | 85 | const queryParams = new URLSearchParams(location.search); |
| 86 | if (gameChapters != undefined && !queryParams.get("chapter")) { | 86 | if (gameChapters != undefined && !queryParams.get('chapter')) { |
| 87 | _fetch_chapters(gameChapters!.chapters[0].id.toString()); | 87 | _fetch_chapters(gameChapters!.chapters[0].id.toString()); |
| 88 | } | 88 | } |
| 89 | }, [gameChapters]) | 89 | }, [gameChapters]); |
| 90 | |||
| 91 | |||
| 92 | 90 | ||
| 93 | return ( | 91 | return ( |
| 94 | <main> | 92 | <main> |
| 95 | <Helmet> | 93 | <Helmet> |
| 96 | <title>LPHUB | Maplist</title> | 94 | <title>LPHUB | Maplist</title> |
| 97 | </Helmet> | 95 | </Helmet> |
| 98 | <section style={{ marginTop: "20px" }}> | 96 | <section style={{ marginTop: '20px' }}> |
| 99 | <Link to="/games"> | 97 | <Link to="/games"> |
| 100 | <button className="nav-button" style={{ borderRadius: "20px" }}> | 98 | <button className="nav-button" style={{ borderRadius: '20px' }}> |
| 101 | <i className="triangle"></i> | 99 | <i className="triangle"></i> |
| 102 | <span>Games List</span> | 100 | <span>Games List</span> |
| 103 | </button> | 101 | </button> |
| @@ -117,7 +115,7 @@ const Maplist: React.FC = () => { | |||
| 117 | <h2 className="portal-count"> | 115 | <h2 className="portal-count"> |
| 118 | { | 116 | { |
| 119 | game?.category_portals.find( | 117 | game?.category_portals.find( |
| 120 | (obj) => obj.category.id === catNum + 1 | 118 | obj => obj.category.id === catNum + 1 |
| 121 | )?.portal_count | 119 | )?.portal_count |
| 122 | } | 120 | } |
| 123 | </h2> | 121 | </h2> |
| @@ -125,7 +123,19 @@ const Maplist: React.FC = () => { | |||
| 125 | </div> | 123 | </div> |
| 126 | <div className="game-header-categories"> | 124 | <div className="game-header-categories"> |
| 127 | {game?.category_portals.map((cat, index) => ( | 125 | {game?.category_portals.map((cat, index) => ( |
| 128 | <button key={index} className={currentlySelected == cat.category.id || cat.category.id - 1 == catNum && !hasClicked ? "game-cat-button selected" : "game-cat-button"} onClick={() => { setCatNum(cat.category.id - 1); _update_currently_selected(cat.category.id) }}> | 126 | <button |
| 127 | key={index} | ||
| 128 | className={ | ||
| 129 | currentlySelected == cat.category.id || | ||
| 130 | (cat.category.id - 1 == catNum && !hasClicked) | ||
| 131 | ? 'game-cat-button selected' | ||
| 132 | : 'game-cat-button' | ||
| 133 | } | ||
| 134 | onClick={() => { | ||
| 135 | setCatNum(cat.category.id - 1); | ||
| 136 | _update_currently_selected(cat.category.id); | ||
| 137 | }} | ||
| 138 | > | ||
| 129 | <span>{cat.category.name}</span> | 139 | <span>{cat.category.name}</span> |
| 130 | </button> | 140 | </button> |
| 131 | ))} | 141 | ))} |
| @@ -136,45 +146,88 @@ const Maplist: React.FC = () => { | |||
| 136 | <div> | 146 | <div> |
| 137 | <section className="chapter-select-container"> | 147 | <section className="chapter-select-container"> |
| 138 | <div> | 148 | <div> |
| 139 | <span style={{ fontSize: "18px", transform: "translateY(5px)", display: "block", marginTop: "10px" }}>{curChapter?.chapter.name.split(" - ")[0]}</span> | 149 | <span |
| 150 | style={{ | ||
| 151 | fontSize: '18px', | ||
| 152 | transform: 'translateY(5px)', | ||
| 153 | display: 'block', | ||
| 154 | marginTop: '10px', | ||
| 155 | }} | ||
| 156 | > | ||
| 157 | {curChapter?.chapter.name.split(' - ')[0]} | ||
| 158 | </span> | ||
| 140 | </div> | 159 | </div> |
| 141 | <div onClick={_handle_dropdown_click} className="dropdown"> | 160 | <div onClick={_handle_dropdown_click} className="dropdown"> |
| 142 | <span>{curChapter?.chapter.name.split(" - ")[1]}</span> | 161 | <span>{curChapter?.chapter.name.split(' - ')[1]}</span> |
| 143 | <i className="triangle"></i> | 162 | <i className="triangle"></i> |
| 144 | </div> | 163 | </div> |
| 145 | <div className="dropdown-elements" style={{ display: dropdownActive }}> | 164 | <div |
| 165 | className="dropdown-elements" | ||
| 166 | style={{ display: dropdownActive }} | ||
| 167 | > | ||
| 146 | {gameChapters?.chapters.map((chapter, i) => { | 168 | {gameChapters?.chapters.map((chapter, i) => { |
| 147 | return <div className="dropdown-element" onClick={() => { _fetch_chapters(chapter.id.toString()); _handle_dropdown_click() }}>{chapter.name}</div> | 169 | return ( |
| 148 | }) | 170 | <div |
| 149 | 171 | className="dropdown-element" | |
| 150 | } | 172 | onClick={() => { |
| 173 | _fetch_chapters(chapter.id.toString()); | ||
| 174 | _handle_dropdown_click(); | ||
| 175 | }} | ||
| 176 | > | ||
| 177 | {chapter.name} | ||
| 178 | </div> | ||
| 179 | ); | ||
| 180 | })} | ||
| 151 | </div> | 181 | </div> |
| 152 | </section> | 182 | </section> |
| 153 | <section className="maplist"> | 183 | <section className="maplist"> |
| 154 | {curChapter?.maps.map((map, i) => { | 184 | {curChapter?.maps.map((map, i) => { |
| 155 | return <div className="maplist-entry"> | 185 | return ( |
| 156 | <Link to={`/maps/${map.id}`}> | 186 | <div className="maplist-entry"> |
| 157 | <span>{map.name}</span> | 187 | <Link to={`/maps/${map.id}`}> |
| 158 | <div className="map-entry-image" style={{ backgroundImage: `url(${map.image})` }}> | 188 | <span>{map.name}</span> |
| 159 | <div className="blur map"> | 189 | <div |
| 160 | <span>{map.is_disabled ? map.category_portals[0].portal_count : map.category_portals.find( | 190 | className="map-entry-image" |
| 161 | (obj) => obj.category.id === catNum + 1 | 191 | style={{ backgroundImage: `url(${map.image})` }} |
| 162 | )?.portal_count}</span> | 192 | > |
| 163 | <span>portals</span> | 193 | <div className="blur map"> |
| 194 | <span> | ||
| 195 | {map.is_disabled | ||
| 196 | ? map.category_portals[0].portal_count | ||
| 197 | : map.category_portals.find( | ||
| 198 | obj => obj.category.id === catNum + 1 | ||
| 199 | )?.portal_count} | ||
| 200 | </span> | ||
| 201 | <span>portals</span> | ||
| 202 | </div> | ||
| 164 | </div> | 203 | </div> |
| 165 | </div> | 204 | <div className="difficulty-bar"> |
| 166 | <div className="difficulty-bar"> | 205 | {/* <span>Difficulty:</span> */} |
| 167 | {/* <span>Difficulty:</span> */} | 206 | <div |
| 168 | <div className={map.difficulty == 0 ? "one" : map.difficulty == 1 ? "two" : map.difficulty == 2 ? "three" : map.difficulty == 3 ? "four" : map.difficulty == 4 ? "five" : "one"}> | 207 | className={ |
| 169 | <div className="difficulty-point"></div> | 208 | map.difficulty == 0 |
| 170 | <div className="difficulty-point"></div> | 209 | ? 'one' |
| 171 | <div className="difficulty-point"></div> | 210 | : map.difficulty == 1 |
| 172 | <div className="difficulty-point"></div> | 211 | ? 'two' |
| 173 | <div className="difficulty-point"></div> | 212 | : map.difficulty == 2 |
| 213 | ? 'three' | ||
| 214 | : map.difficulty == 3 | ||
| 215 | ? 'four' | ||
| 216 | : map.difficulty == 4 | ||
| 217 | ? 'five' | ||
| 218 | : 'one' | ||
| 219 | } | ||
| 220 | > | ||
| 221 | <div className="difficulty-point"></div> | ||
| 222 | <div className="difficulty-point"></div> | ||
| 223 | <div className="difficulty-point"></div> | ||
| 224 | <div className="difficulty-point"></div> | ||
| 225 | <div className="difficulty-point"></div> | ||
| 226 | </div> | ||
| 174 | </div> | 227 | </div> |
| 175 | </div> | 228 | </Link> |
| 176 | </Link> | 229 | </div> |
| 177 | </div> | 230 | ); |
| 178 | })} | 231 | })} |
| 179 | </section> | 232 | </section> |
| 180 | </div> | 233 | </div> |
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx index fb13563..51a2020 100644 --- a/frontend/src/pages/Maps.tsx +++ b/frontend/src/pages/Maps.tsx | |||
| @@ -9,26 +9,31 @@ 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 { |
| 15 | token?: string; | 15 | token?: string; |
| 16 | isModerator: boolean; | 16 | isModerator: boolean; |
| 17 | }; | 17 | } |
| 18 | 18 | ||
| 19 | const Maps: React.FC<MapProps> = ({ token, isModerator }) => { | 19 | const Maps: React.FC<MapProps> = ({ token, isModerator }) => { |
| 20 | |||
| 21 | const [selectedRun, setSelectedRun] = React.useState<number>(0); | 20 | const [selectedRun, setSelectedRun] = React.useState<number>(0); |
| 22 | 21 | ||
| 23 | const [mapSummaryData, setMapSummaryData] = React.useState<MapSummary | undefined>(undefined); | 22 | const [mapSummaryData, setMapSummaryData] = React.useState< |
| 24 | const [mapLeaderboardData, setMapLeaderboardData] = React.useState<MapLeaderboard | undefined>(undefined); | 23 | MapSummary | undefined |
| 25 | const [mapDiscussionsData, setMapDiscussionsData] = React.useState<MapDiscussions | undefined>(undefined); | 24 | >(undefined); |
| 25 | const [mapLeaderboardData, setMapLeaderboardData] = React.useState< | ||
| 26 | MapLeaderboard | undefined | ||
| 27 | >(undefined); | ||
| 28 | const [mapDiscussionsData, setMapDiscussionsData] = React.useState< | ||
| 29 | MapDiscussions | undefined | ||
| 30 | >(undefined); | ||
| 26 | 31 | ||
| 27 | const [navState, setNavState] = React.useState<number>(0); | 32 | const [navState, setNavState] = React.useState<number>(0); |
| 28 | 33 | ||
| 29 | const location = useLocation(); | 34 | const location = useLocation(); |
| 30 | 35 | ||
| 31 | const mapID = location.pathname.split("/")[2]; | 36 | const mapID = location.pathname.split('/')[2]; |
| 32 | 37 | ||
| 33 | const _fetch_map_summary = async () => { | 38 | const _fetch_map_summary = async () => { |
| 34 | const mapSummary = await API.get_map_summary(mapID); | 39 | const mapSummary = await API.get_map_summary(mapID); |
| @@ -36,7 +41,7 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => { | |||
| 36 | }; | 41 | }; |
| 37 | 42 | ||
| 38 | const _fetch_map_leaderboards = async () => { | 43 | const _fetch_map_leaderboards = async () => { |
| 39 | const mapLeaderboards = await API.get_map_leaderboard(mapID, "1"); | 44 | const mapLeaderboards = await API.get_map_leaderboard(mapID, '1'); |
| 40 | setMapLeaderboardData(mapLeaderboards); | 45 | setMapLeaderboardData(mapLeaderboards); |
| 41 | }; | 46 | }; |
| 42 | 47 | ||
| @@ -56,19 +61,36 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => { | |||
| 56 | return ( | 61 | return ( |
| 57 | <> | 62 | <> |
| 58 | <main> | 63 | <main> |
| 59 | <section id='section1' className='summary1'> | 64 | <section id="section1" className="summary1"> |
| 60 | <div> | 65 | <div> |
| 61 | <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 20px 20px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> | 66 | <Link to="/games"> |
| 67 | <button | ||
| 68 | className="nav-button" | ||
| 69 | style={{ borderRadius: '20px 20px 20px 20px' }} | ||
| 70 | > | ||
| 71 | <i className="triangle"></i> | ||
| 72 | <span>Games List</span> | ||
| 73 | </button> | ||
| 74 | </Link> | ||
| 62 | </div> | 75 | </div> |
| 63 | </section> | 76 | </section> |
| 64 | 77 | ||
| 65 | <section id='section2' className='summary1'> | 78 | <section id="section2" className="summary1"> |
| 66 | <button className='nav-button'><img src={PortalIcon} alt="" /><span>Summary</span></button> | 79 | <button className="nav-button"> |
| 67 | <button className='nav-button'><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> | 80 | <img src={PortalIcon} alt="" /> |
| 68 | <button className='nav-button'><img src={ChatIcon} alt="" /><span>Discussions</span></button> | 81 | <span>Summary</span> |
| 82 | </button> | ||
| 83 | <button className="nav-button"> | ||
| 84 | <img src={FlagIcon} alt="" /> | ||
| 85 | <span>Leaderboards</span> | ||
| 86 | </button> | ||
| 87 | <button className="nav-button"> | ||
| 88 | <img src={ChatIcon} alt="" /> | ||
| 89 | <span>Discussions</span> | ||
| 90 | </button> | ||
| 69 | </section> | 91 | </section> |
| 70 | 92 | ||
| 71 | <section id='section6' className='summary2' /> | 93 | <section id="section6" className="summary2" /> |
| 72 | </main> | 94 | </main> |
| 73 | </> | 95 | </> |
| 74 | ); | 96 | ); |
| @@ -80,29 +102,80 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => { | |||
| 80 | <title>LPHUB | {mapSummaryData.map.map_name}</title> | 102 | <title>LPHUB | {mapSummaryData.map.map_name}</title> |
| 81 | <meta name="description" content={mapSummaryData.map.map_name} /> | 103 | <meta name="description" content={mapSummaryData.map.map_name} /> |
| 82 | </Helmet> | 104 | </Helmet> |
| 83 | {isModerator && <ModMenu token={token} data={mapSummaryData} selectedRun={selectedRun} mapID={mapID} />} | 105 | {isModerator && ( |
| 84 | 106 | <ModMenu | |
| 85 | <div id='background-image'> | 107 | token={token} |
| 108 | data={mapSummaryData} | ||
| 109 | selectedRun={selectedRun} | ||
| 110 | mapID={mapID} | ||
| 111 | /> | ||
| 112 | )} | ||
| 113 | |||
| 114 | <div id="background-image"> | ||
| 86 | <img src={mapSummaryData.map.image} alt="" /> | 115 | <img src={mapSummaryData.map.image} alt="" /> |
| 87 | </div> | 116 | </div> |
| 88 | <main> | 117 | <main> |
| 89 | <section id='section1' className='summary1'> | 118 | <section id="section1" className="summary1"> |
| 90 | <div> | 119 | <div> |
| 91 | <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 0px 0px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> | 120 | <Link to="/games"> |
| 92 | <Link to={`/games/${mapSummaryData.map.is_coop ? "2" : "1"}?chapter=${mapSummaryData.map.chapter_name.split(" ")[1]}`}><button className='nav-button' style={{ borderRadius: "0px 20px 20px 0px", marginLeft: "2px" }}><i className='triangle'></i><span>{mapSummaryData.map.chapter_name}</span></button></Link> | 121 | <button |
| 93 | <br /><span><b>{mapSummaryData.map.map_name}</b></span> | 122 | className="nav-button" |
| 123 | style={{ borderRadius: '20px 0px 0px 20px' }} | ||
| 124 | > | ||
| 125 | <i className="triangle"></i> | ||
| 126 | <span>Games List</span> | ||
| 127 | </button> | ||
| 128 | </Link> | ||
| 129 | <Link | ||
| 130 | to={`/games/${mapSummaryData.map.is_coop ? '2' : '1'}?chapter=${mapSummaryData.map.chapter_name.split(' ')[1]}`} | ||
| 131 | > | ||
| 132 | <button | ||
| 133 | className="nav-button" | ||
| 134 | style={{ borderRadius: '0px 20px 20px 0px', marginLeft: '2px' }} | ||
| 135 | > | ||
| 136 | <i className="triangle"></i> | ||
| 137 | <span>{mapSummaryData.map.chapter_name}</span> | ||
| 138 | </button> | ||
| 139 | </Link> | ||
| 140 | <br /> | ||
| 141 | <span> | ||
| 142 | <b>{mapSummaryData.map.map_name}</b> | ||
| 143 | </span> | ||
| 94 | </div> | 144 | </div> |
| 95 | </section> | 145 | </section> |
| 96 | 146 | ||
| 97 | <section id='section2' className='summary1'> | 147 | <section id="section2" className="summary1"> |
| 98 | <button className='nav-button' onClick={() => setNavState(0)}><img src={PortalIcon} alt="" /><span>Summary</span></button> | 148 | <button className="nav-button" onClick={() => setNavState(0)}> |
| 99 | <button className='nav-button' onClick={() => setNavState(1)}><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> | 149 | <img src={PortalIcon} alt="" /> |
| 100 | <button className='nav-button' onClick={() => setNavState(2)}><img src={ChatIcon} alt="" /><span>Discussions</span></button> | 150 | <span>Summary</span> |
| 151 | </button> | ||
| 152 | <button className="nav-button" onClick={() => setNavState(1)}> | ||
| 153 | <img src={FlagIcon} alt="" /> | ||
| 154 | <span>Leaderboards</span> | ||
| 155 | </button> | ||
| 156 | <button className="nav-button" onClick={() => setNavState(2)}> | ||
| 157 | <img src={ChatIcon} alt="" /> | ||
| 158 | <span>Discussions</span> | ||
| 159 | </button> | ||
| 101 | </section> | 160 | </section> |
| 102 | 161 | ||
| 103 | {navState === 0 && <Summary selectedRun={selectedRun} setSelectedRun={setSelectedRun} data={mapSummaryData} />} | 162 | {navState === 0 && ( |
| 163 | <Summary | ||
| 164 | selectedRun={selectedRun} | ||
| 165 | setSelectedRun={setSelectedRun} | ||
| 166 | data={mapSummaryData} | ||
| 167 | /> | ||
| 168 | )} | ||
| 104 | {navState === 1 && <Leaderboards mapID={mapID} />} | 169 | {navState === 1 && <Leaderboards mapID={mapID} />} |
| 105 | {navState === 2 && <Discussions data={mapDiscussionsData} token={token} isModerator={isModerator} mapID={mapID} onRefresh={() => _fetch_map_discussions()} />} | 170 | {navState === 2 && ( |
| 171 | <Discussions | ||
| 172 | data={mapDiscussionsData} | ||
| 173 | token={token} | ||
| 174 | isModerator={isModerator} | ||
| 175 | mapID={mapID} | ||
| 176 | onRefresh={() => _fetch_map_discussions()} | ||
| 177 | /> | ||
| 178 | )} | ||
| 106 | </main> | 179 | </main> |
| 107 | </> | 180 | </> |
| 108 | ); | 181 | ); |
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 48233bf..7e3d603 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx | |||
| @@ -2,16 +2,28 @@ 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 { |
| 6 | SteamIcon, | ||
| 7 | TwitchIcon, | ||
| 8 | YouTubeIcon, | ||
| 9 | PortalIcon, | ||
| 10 | FlagIcon, | ||
| 11 | StatisticsIcon, | ||
| 12 | SortIcon, | ||
| 13 | ThreedotIcon, | ||
| 14 | DownloadIcon, | ||
| 15 | HistoryIcon, | ||
| 16 | DeleteIcon, | ||
| 17 | } from '@images/Images'; | ||
| 6 | import { UserProfile } from '@customTypes/Profile'; | 18 | import { UserProfile } from '@customTypes/Profile'; |
| 7 | import { Game, GameChapters } from '@customTypes/Game'; | 19 | import { Game, GameChapters } from '@customTypes/Game'; |
| 8 | import { Map } from '@customTypes/Map'; | 20 | import { Map } from '@customTypes/Map'; |
| 9 | import { ticks_to_time } from '@utils/Time'; | 21 | import { ticks_to_time } from '@utils/Time'; |
| 10 | import "@css/Profile.css"; | 22 | import '@css/Profile.css'; |
| 11 | import { API } from '@api/Api'; | 23 | import { API } from '@api/Api'; |
| 12 | import useConfirm from '@hooks/UseConfirm'; | 24 | import useConfirm from '@hooks/UseConfirm'; |
| 13 | import useMessage from '@hooks/UseMessage'; | 25 | import useMessage from '@hooks/UseMessage'; |
| 14 | import useMessageLoad from "@hooks/UseMessageLoad"; | 26 | import useMessageLoad from '@hooks/UseMessageLoad'; |
| 15 | 27 | ||
| 16 | interface ProfileProps { | 28 | interface ProfileProps { |
| 17 | profile?: UserProfile; | 29 | profile?: UserProfile; |
| @@ -20,17 +32,25 @@ interface ProfileProps { | |||
| 20 | onDeleteRecord: () => void; | 32 | onDeleteRecord: () => void; |
| 21 | } | 33 | } |
| 22 | 34 | ||
| 23 | const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRecord }) => { | 35 | const Profile: React.FC<ProfileProps> = ({ |
| 36 | profile, | ||
| 37 | token, | ||
| 38 | gameData, | ||
| 39 | onDeleteRecord, | ||
| 40 | }) => { | ||
| 24 | const { confirm, ConfirmDialogComponent } = useConfirm(); | 41 | const { confirm, ConfirmDialogComponent } = useConfirm(); |
| 25 | const { message, MessageDialogComponent } = useMessage(); | 42 | const { message, MessageDialogComponent } = useMessage(); |
| 26 | const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = useMessageLoad(); | 43 | const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = |
| 44 | useMessageLoad(); | ||
| 27 | const [navState, setNavState] = React.useState(0); | 45 | const [navState, setNavState] = React.useState(0); |
| 28 | const [pageNumber, setPageNumber] = React.useState(1); | 46 | const [pageNumber, setPageNumber] = React.useState(1); |
| 29 | const [pageMax, setPageMax] = React.useState(0); | 47 | const [pageMax, setPageMax] = React.useState(0); |
| 30 | 48 | ||
| 31 | const [game, setGame] = React.useState("0") | 49 | const [game, setGame] = React.useState('0'); |
| 32 | const [chapter, setChapter] = React.useState("0") | 50 | const [chapter, setChapter] = React.useState('0'); |
| 33 | const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); | 51 | const [chapterData, setChapterData] = React.useState<GameChapters | null>( |
| 52 | null | ||
| 53 | ); | ||
| 34 | const [maps, setMaps] = React.useState<Map[]>([]); | 54 | const [maps, setMaps] = React.useState<Map[]>([]); |
| 35 | 55 | ||
| 36 | const navigate = useNavigate(); | 56 | const navigate = useNavigate(); |
| @@ -42,17 +62,17 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 42 | }; | 62 | }; |
| 43 | 63 | ||
| 44 | const _get_game_chapters = async () => { | 64 | const _get_game_chapters = async () => { |
| 45 | if (game && game !== "0") { | 65 | if (game && game !== '0') { |
| 46 | const gameChapters = await API.get_games_chapters(game); | 66 | const gameChapters = await API.get_games_chapters(game); |
| 47 | setChapterData(gameChapters); | 67 | setChapterData(gameChapters); |
| 48 | } else if (game && game === "0") { | 68 | } else if (game && game === '0') { |
| 49 | setPageMax(Math.ceil(profile!.records.length / 20)); | 69 | setPageMax(Math.ceil(profile!.records.length / 20)); |
| 50 | setPageNumber(1); | 70 | setPageNumber(1); |
| 51 | } | 71 | } |
| 52 | }; | 72 | }; |
| 53 | 73 | ||
| 54 | const _get_game_maps = async () => { | 74 | const _get_game_maps = async () => { |
| 55 | if (chapter === "0") { | 75 | if (chapter === '0') { |
| 56 | const gameMaps = await API.get_game_maps(game); | 76 | const gameMaps = await API.get_game_maps(game); |
| 57 | setMaps(gameMaps); | 77 | setMaps(gameMaps); |
| 58 | setPageMax(Math.ceil(gameMaps.length / 20)); | 78 | setPageMax(Math.ceil(gameMaps.length / 20)); |
| @@ -66,28 +86,31 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 66 | }; | 86 | }; |
| 67 | 87 | ||
| 68 | const _delete_submission = async (map_id: number, record_id: number) => { | 88 | const _delete_submission = async (map_id: number, record_id: number) => { |
| 69 | const userConfirmed = await confirm("Delete Record", "Are you sure you want to delete this record?"); | 89 | const userConfirmed = await confirm( |
| 90 | 'Delete Record', | ||
| 91 | 'Are you sure you want to delete this record?' | ||
| 92 | ); | ||
| 70 | 93 | ||
| 71 | if (!userConfirmed) { | 94 | if (!userConfirmed) { |
| 72 | return; | 95 | return; |
| 73 | } | 96 | } |
| 74 | 97 | ||
| 75 | messageLoad("Deleting..."); | 98 | messageLoad('Deleting...'); |
| 76 | 99 | ||
| 77 | const api_success = await API.delete_map_record(token!, map_id, record_id); | 100 | const api_success = await API.delete_map_record(token!, map_id, record_id); |
| 78 | messageLoadClose(); | 101 | messageLoadClose(); |
| 79 | if (api_success) { | 102 | if (api_success) { |
| 80 | await message("Delete Record", "Successfully deleted record."); | 103 | await message('Delete Record', 'Successfully deleted record.'); |
| 81 | onDeleteRecord(); | 104 | onDeleteRecord(); |
| 82 | } else { | 105 | } else { |
| 83 | await message("Delete Record", "Could not delete record."); | 106 | await message('Delete Record', 'Could not delete record.'); |
| 84 | } | 107 | } |
| 85 | }; | 108 | }; |
| 86 | 109 | ||
| 87 | React.useEffect(() => { | 110 | React.useEffect(() => { |
| 88 | if (!profile) { | 111 | if (!profile) { |
| 89 | navigate("/"); | 112 | navigate('/'); |
| 90 | }; | 113 | } |
| 91 | }, [profile]); | 114 | }, [profile]); |
| 92 | 115 | ||
| 93 | React.useEffect(() => { | 116 | React.useEffect(() => { |
| @@ -97,16 +120,14 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 97 | }, [profile, game]); | 120 | }, [profile, game]); |
| 98 | 121 | ||
| 99 | React.useEffect(() => { | 122 | React.useEffect(() => { |
| 100 | if (profile && game !== "0") { | 123 | if (profile && game !== '0') { |
| 101 | _get_game_maps(); | 124 | _get_game_maps(); |
| 102 | } | 125 | } |
| 103 | }, [profile, game, chapter, chapterData]) | 126 | }, [profile, game, chapter, chapterData]); |
| 104 | 127 | ||
| 105 | if (!profile) { | 128 | if (!profile) { |
| 106 | return ( | 129 | return <></>; |
| 107 | <></> | 130 | } |
| 108 | ); | ||
| 109 | }; | ||
| 110 | 131 | ||
| 111 | return ( | 132 | return ( |
| 112 | <div> | 133 | <div> |
| @@ -119,230 +140,490 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 119 | {ConfirmDialogComponent} | 140 | {ConfirmDialogComponent} |
| 120 | 141 | ||
| 121 | <main> | 142 | <main> |
| 122 | <section id='section1' className='profile'> | 143 | <section id="section1" className="profile"> |
| 123 | 144 | {profile.profile ? ( | |
| 124 | {profile.profile | 145 | <div id="profile-image" onClick={_update_profile}> |
| 125 | ? ( | 146 | <img src={profile.avatar_link} alt="profile-image"></img> |
| 126 | <div id='profile-image' onClick={_update_profile}> | 147 | <span>Refresh</span> |
| 127 | <img src={profile.avatar_link} alt="profile-image"></img> | 148 | </div> |
| 128 | <span>Refresh</span> | 149 | ) : ( |
| 129 | </div> | 150 | <div> |
| 130 | ) : ( | 151 | <img src={profile.avatar_link} alt="profile-image"></img> |
| 131 | <div> | 152 | </div> |
| 132 | <img src={profile.avatar_link} alt="profile-image"></img> | 153 | )} |
| 133 | </div> | ||
| 134 | )} | ||
| 135 | 154 | ||
| 136 | <div id='profile-top'> | 155 | <div id="profile-top"> |
| 137 | <div> | 156 | <div> |
| 138 | <div>{profile.user_name}</div> | 157 | <div>{profile.user_name}</div> |
| 139 | <div> | 158 | <div> |
| 140 | {profile.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`} alt={profile.country_code} />} | 159 | {profile.country_code === 'XX' ? ( |
| 160 | '' | ||
| 161 | ) : ( | ||
| 162 | <img | ||
| 163 | src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`} | ||
| 164 | alt={profile.country_code} | ||
| 165 | /> | ||
| 166 | )} | ||
| 141 | </div> | 167 | </div> |
| 142 | <div> | 168 | <div> |
| 143 | {profile.titles.map(e => ( | 169 | {profile.titles.map(e => ( |
| 144 | <span className="titles" style={{ backgroundColor: `#${e.color}` }}> | 170 | <span |
| 171 | className="titles" | ||
| 172 | style={{ backgroundColor: `#${e.color}` }} | ||
| 173 | > | ||
| 145 | {e.name} | 174 | {e.name} |
| 146 | </span> | 175 | </span> |
| 147 | ))} | 176 | ))} |
| 148 | </div> | 177 | </div> |
| 149 | </div> | 178 | </div> |
| 150 | <div> | 179 | <div> |
| 151 | {profile.links.steam === "-" ? "" : <a href={profile.links.steam}><img src={SteamIcon} alt="Steam" /></a>} | 180 | {profile.links.steam === '-' ? ( |
| 152 | {profile.links.twitch === "-" ? "" : <a href={profile.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} | 181 | '' |
| 153 | {profile.links.youtube === "-" ? "" : <a href={profile.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} | 182 | ) : ( |
| 154 | {profile.links.p2sr === "-" ? "" : <a href={profile.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} | 183 | <a href={profile.links.steam}> |
| 184 | <img src={SteamIcon} alt="Steam" /> | ||
| 185 | </a> | ||
| 186 | )} | ||
| 187 | {profile.links.twitch === '-' ? ( | ||
| 188 | '' | ||
| 189 | ) : ( | ||
| 190 | <a href={profile.links.twitch}> | ||
| 191 | <img src={TwitchIcon} alt="Twitch" /> | ||
| 192 | </a> | ||
| 193 | )} | ||
| 194 | {profile.links.youtube === '-' ? ( | ||
| 195 | '' | ||
| 196 | ) : ( | ||
| 197 | <a href={profile.links.youtube}> | ||
| 198 | <img src={YouTubeIcon} alt="Youtube" /> | ||
| 199 | </a> | ||
| 200 | )} | ||
| 201 | {profile.links.p2sr === '-' ? ( | ||
| 202 | '' | ||
| 203 | ) : ( | ||
| 204 | <a href={profile.links.p2sr}> | ||
| 205 | <img src={PortalIcon} alt="P2SR" style={{ padding: '0' }} /> | ||
| 206 | </a> | ||
| 207 | )} | ||
| 155 | </div> | 208 | </div> |
| 156 | |||
| 157 | </div> | 209 | </div> |
| 158 | <div id='profile-bottom'> | 210 | <div id="profile-bottom"> |
| 159 | <div> | 211 | <div> |
| 160 | <span>Overall</span> | 212 | <span>Overall</span> |
| 161 | <span>{profile.rankings.overall.rank === 0 ? "N/A " : "#" + profile.rankings.overall.rank + " "} | 213 | <span> |
| 162 | <span>({profile.rankings.overall.completion_count}/{profile.rankings.overall.completion_total})</span> | 214 | {profile.rankings.overall.rank === 0 |
| 215 | ? 'N/A ' | ||
| 216 | : '#' + profile.rankings.overall.rank + ' '} | ||
| 217 | <span> | ||
| 218 | ({profile.rankings.overall.completion_count}/ | ||
| 219 | {profile.rankings.overall.completion_total}) | ||
| 220 | </span> | ||
| 163 | </span> | 221 | </span> |
| 164 | </div> | 222 | </div> |
| 165 | <div> | 223 | <div> |
| 166 | <span>Singleplayer</span> | 224 | <span>Singleplayer</span> |
| 167 | <span>{profile.rankings.singleplayer.rank === 0 ? "N/A " : "#" + profile.rankings.singleplayer.rank + " "} | 225 | <span> |
| 168 | <span>({profile.rankings.singleplayer.completion_count}/{profile.rankings.singleplayer.completion_total})</span> | 226 | {profile.rankings.singleplayer.rank === 0 |
| 227 | ? 'N/A ' | ||
| 228 | : '#' + profile.rankings.singleplayer.rank + ' '} | ||
| 229 | <span> | ||
| 230 | ({profile.rankings.singleplayer.completion_count}/ | ||
| 231 | {profile.rankings.singleplayer.completion_total}) | ||
| 232 | </span> | ||
| 169 | </span> | 233 | </span> |
| 170 | </div> | 234 | </div> |
| 171 | <div> | 235 | <div> |
| 172 | <span>Cooperative</span> | 236 | <span>Cooperative</span> |
| 173 | <span>{profile.rankings.cooperative.rank === 0 ? "N/A " : "#" + profile.rankings.cooperative.rank + " "} | 237 | <span> |
| 174 | <span>({profile.rankings.cooperative.completion_count}/{profile.rankings.cooperative.completion_total})</span> | 238 | {profile.rankings.cooperative.rank === 0 |
| 239 | ? 'N/A ' | ||
| 240 | : '#' + profile.rankings.cooperative.rank + ' '} | ||
| 241 | <span> | ||
| 242 | ({profile.rankings.cooperative.completion_count}/ | ||
| 243 | {profile.rankings.cooperative.completion_total}) | ||
| 244 | </span> | ||
| 175 | </span> | 245 | </span> |
| 176 | </div> | 246 | </div> |
| 177 | </div> | 247 | </div> |
| 178 | </section> | 248 | </section> |
| 179 | 249 | ||
| 180 | 250 | <section id="section2" className="profile"> | |
| 181 | <section id='section2' className='profile'> | 251 | <button onClick={() => setNavState(0)}> |
| 182 | <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" /> Player Records</button> | 252 | <img src={FlagIcon} alt="" /> |
| 183 | <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" /> Statistics</button> | 253 | Player Records |
| 254 | </button> | ||
| 255 | <button onClick={() => setNavState(1)}> | ||
| 256 | <img src={StatisticsIcon} alt="" /> | ||
| 257 | Statistics | ||
| 258 | </button> | ||
| 184 | </section> | 259 | </section> |
| 185 | 260 | ||
| 186 | 261 | <section id="section3" className="profile1"> | |
| 187 | 262 | <div id="profileboard-nav"> | |
| 188 | 263 | {gameData === null ? ( | |
| 189 | 264 | <select>error</select> | |
| 190 | <section id='section3' className='profile1'> | 265 | ) : ( |
| 191 | <div id='profileboard-nav'> | 266 | <select |
| 192 | {gameData === null ? <select>error</select> : | 267 | id="select-game" |
| 193 | |||
| 194 | <select id='select-game' | ||
| 195 | onChange={() => { | 268 | onChange={() => { |
| 196 | setGame((document.querySelector('#select-game') as HTMLInputElement).value); | 269 | setGame( |
| 197 | setChapter("0"); | 270 | (document.querySelector('#select-game') as HTMLInputElement) |
| 198 | const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; | 271 | .value |
| 272 | ); | ||
| 273 | setChapter('0'); | ||
| 274 | const chapterSelect = document.querySelector( | ||
| 275 | '#select-chapter' | ||
| 276 | ) as HTMLSelectElement; | ||
| 199 | if (chapterSelect) { | 277 | if (chapterSelect) { |
| 200 | chapterSelect.value = "0"; | 278 | chapterSelect.value = '0'; |
| 201 | } | 279 | } |
| 202 | }}> | 280 | }} |
| 203 | <option value={0} key={0}>All Scores</option> | 281 | > |
| 282 | <option value={0} key={0}> | ||
| 283 | All Scores | ||
| 284 | </option> | ||
| 204 | {gameData.map((e, i) => ( | 285 | {gameData.map((e, i) => ( |
| 205 | <option value={e.id} key={i + 1}>{e.name}</option> | 286 | <option value={e.id} key={i + 1}> |
| 206 | ))}</select> | 287 | {e.name} |
| 207 | } | 288 | </option> |
| 289 | ))} | ||
| 290 | </select> | ||
| 291 | )} | ||
| 208 | 292 | ||
| 209 | {game === "0" ? | 293 | {game === '0' ? ( |
| 210 | <select disabled> | 294 | <select disabled> |
| 211 | <option>All Chapters</option> | 295 | <option>All Chapters</option> |
| 212 | </select> | 296 | </select> |
| 213 | : chapterData === null ? <select></select> : | 297 | ) : chapterData === null ? ( |
| 214 | 298 | <select></select> | |
| 215 | <select id='select-chapter' | 299 | ) : ( |
| 216 | onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> | 300 | <select |
| 217 | <option value="0" key="0">All Chapters</option> | 301 | id="select-chapter" |
| 218 | {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( | 302 | onChange={() => |
| 219 | <option value={e.id} key={i + 1}>{e.name}</option> | 303 | setChapter( |
| 220 | ))}</select> | 304 | ( |
| 221 | } | 305 | document.querySelector( |
| 306 | '#select-chapter' | ||
| 307 | ) as HTMLInputElement | ||
| 308 | ).value | ||
| 309 | ) | ||
| 310 | } | ||
| 311 | > | ||
| 312 | <option value="0" key="0"> | ||
| 313 | All Chapters | ||
| 314 | </option> | ||
| 315 | {chapterData.chapters | ||
| 316 | .filter(e => e.is_disabled === false) | ||
| 317 | .map((e, i) => ( | ||
| 318 | <option value={e.id} key={i + 1}> | ||
| 319 | {e.name} | ||
| 320 | </option> | ||
| 321 | ))} | ||
| 322 | </select> | ||
| 323 | )} | ||
| 222 | </div> | 324 | </div> |
| 223 | <div id='profileboard-top'> | 325 | <div id="profileboard-top"> |
| 224 | <span><span>Map Name</span><img src={SortIcon} alt="" /></span> | 326 | <span> |
| 225 | <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> | 327 | <span>Map Name</span> |
| 226 | <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> | 328 | <img src={SortIcon} alt="" /> |
| 227 | <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> | 329 | </span> |
| 330 | <span style={{ justifyContent: 'center' }}> | ||
| 331 | <span>Portals</span> | ||
| 332 | <img src={SortIcon} alt="" /> | ||
| 333 | </span> | ||
| 334 | <span style={{ justifyContent: 'center' }}> | ||
| 335 | <span>WRΔ </span> | ||
| 336 | <img src={SortIcon} alt="" /> | ||
| 337 | </span> | ||
| 338 | <span style={{ justifyContent: 'center' }}> | ||
| 339 | <span>Time</span> | ||
| 340 | <img src={SortIcon} alt="" /> | ||
| 341 | </span> | ||
| 228 | <span> </span> | 342 | <span> </span> |
| 229 | <span><span>Rank</span><img src={SortIcon} alt="" /></span> | 343 | <span> |
| 230 | <span><span>Date</span><img src={SortIcon} alt="" /></span> | 344 | <span>Rank</span> |
| 231 | <div id='page-number'> | 345 | <img src={SortIcon} alt="" /> |
| 346 | </span> | ||
| 347 | <span> | ||
| 348 | <span>Date</span> | ||
| 349 | <img src={SortIcon} alt="" /> | ||
| 350 | </span> | ||
| 351 | <div id="page-number"> | ||
| 232 | <div> | 352 | <div> |
| 233 | <button onClick={() => { | 353 | <button |
| 234 | if (pageNumber !== 1) { | 354 | onClick={() => { |
| 235 | setPageNumber(prevPageNumber => prevPageNumber - 1); | 355 | if (pageNumber !== 1) { |
| 236 | const records = document.querySelectorAll(".profileboard-record"); | 356 | setPageNumber(prevPageNumber => prevPageNumber - 1); |
| 237 | records.forEach((r) => { | 357 | const records = document.querySelectorAll( |
| 238 | (r as HTMLInputElement).style.height = "44px"; | 358 | '.profileboard-record' |
| 239 | }); | 359 | ); |
| 240 | } | 360 | records.forEach(r => { |
| 241 | }} | 361 | (r as HTMLInputElement).style.height = '44px'; |
| 242 | ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> | 362 | }); |
| 243 | <span>{pageNumber}/{pageMax}</span> | 363 | } |
| 244 | <button onClick={() => { | 364 | }} |
| 245 | if (pageNumber !== pageMax) { | 365 | > |
| 246 | setPageNumber(prevPageNumber => prevPageNumber + 1); | 366 | <i |
| 247 | const records = document.querySelectorAll(".profileboard-record"); | 367 | className="triangle" |
| 248 | records.forEach((r) => { | 368 | style={{ position: 'relative', left: '-5px' }} |
| 249 | (r as HTMLInputElement).style.height = "44px"; | 369 | ></i>{' '} |
| 250 | }); | 370 | </button> |
| 251 | } | 371 | <span> |
| 252 | }} | 372 | {pageNumber}/{pageMax} |
| 253 | ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> | 373 | </span> |
| 374 | <button | ||
| 375 | onClick={() => { | ||
| 376 | if (pageNumber !== pageMax) { | ||
| 377 | setPageNumber(prevPageNumber => prevPageNumber + 1); | ||
| 378 | const records = document.querySelectorAll( | ||
| 379 | '.profileboard-record' | ||
| 380 | ); | ||
| 381 | records.forEach(r => { | ||
| 382 | (r as HTMLInputElement).style.height = '44px'; | ||
| 383 | }); | ||
| 384 | } | ||
| 385 | }} | ||
| 386 | > | ||
| 387 | <i | ||
| 388 | className="triangle" | ||
| 389 | style={{ | ||
| 390 | position: 'relative', | ||
| 391 | left: '5px', | ||
| 392 | transform: 'rotate(180deg)', | ||
| 393 | }} | ||
| 394 | ></i>{' '} | ||
| 395 | </button> | ||
| 254 | </div> | 396 | </div> |
| 255 | </div> | 397 | </div> |
| 256 | </div> | 398 | </div> |
| 257 | <hr /> | 399 | <hr /> |
| 258 | <div id='profileboard-records'> | 400 | <div id="profileboard-records"> |
| 259 | 401 | {game === '0' ? ( | |
| 260 | {game === "0" | 402 | profile.records |
| 261 | ? ( | 403 | .sort((a, b) => a.map_id - b.map_id) |
| 262 | 404 | .map((r, index) => | |
| 263 | profile.records.sort((a, b) => a.map_id - b.map_id) | 405 | Math.ceil((index + 1) / 20) === pageNumber ? ( |
| 264 | .map((r, index) => ( | 406 | <button className="profileboard-record" key={index}> |
| 265 | 407 | {r.scores.map((e, i) => ( | |
| 266 | Math.ceil((index + 1) / 20) === pageNumber ? ( | 408 | <> |
| 267 | <button className="profileboard-record" key={index}> | 409 | {i !== 0 ? ( |
| 268 | {r.scores.map((e, i) => (<> | 410 | <hr style={{ gridColumn: '1 / span 8' }} /> |
| 269 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | 411 | ) : ( |
| 270 | 412 | '' | |
| 271 | <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> | 413 | )} |
| 272 | 414 | ||
| 273 | <span style={{ display: "grid" }}>{e.score_count}</span> | 415 | <Link to={`/maps/${r.map_id}`}> |
| 274 | 416 | <span>{r.map_name}</span> | |
| 275 | <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> | 417 | </Link> |
| 276 | <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> | 418 | |
| 277 | <span> </span> | 419 | <span style={{ display: 'grid' }}> |
| 278 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} | 420 | {e.score_count} |
| 279 | <span>{e.date.split("T")[0]}</span> | ||
| 280 | <span style={{ flexDirection: "row-reverse" }}> | ||
| 281 | |||
| 282 | <button style={{ marginRight: "10px" }} onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | ||
| 283 | <button onClick={() => { _delete_submission(r.map_id, e.record_id) }}><img src={DeleteIcon}></img></button> | ||
| 284 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | ||
| 285 | {i === 0 && r.scores.length > 1 ? <button onClick={() => { | ||
| 286 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | ||
| 287 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | ||
| 288 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : | ||
| 289 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | ||
| 290 | } | ||
| 291 | }><img src={HistoryIcon} alt="history" /></button> : ""} | ||
| 292 | |||
| 293 | </span> | 421 | </span> |
| 294 | </>))} | ||
| 295 | |||
| 296 | </button> | ||
| 297 | ) : "" | ||
| 298 | ))) : maps ? | ||
| 299 | 422 | ||
| 300 | maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) | 423 | <span style={{ display: 'grid' }}> |
| 301 | .map((r, index) => { | 424 | {e.score_count - r.map_wr_count > 0 |
| 302 | if (Math.ceil((index + 1) / 20) === pageNumber) { | 425 | ? `+${e.score_count - r.map_wr_count}` |
| 303 | let record = profile.records.find((e) => e.map_id === r.id); | 426 | : `-`} |
| 304 | return record === undefined ? ( | 427 | </span> |
| 305 | <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> | 428 | <span style={{ display: 'grid' }}> |
| 306 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 429 | {ticks_to_time(e.score_time)} |
| 307 | <span style={{ display: "grid" }}>N/A</span> | 430 | </span> |
| 308 | <span style={{ display: "grid" }}>N/A</span> | ||
| 309 | <span>N/A</span> | ||
| 310 | <span> </span> | 431 | <span> </span> |
| 311 | <span>N/A</span> | 432 | {i === 0 ? ( |
| 312 | <span>N/A</span> | 433 | <span>#{r.placement}</span> |
| 313 | <span style={{ flexDirection: "row-reverse" }}></span> | 434 | ) : ( |
| 314 | </button> | ||
| 315 | ) : ( | ||
| 316 | <button className="profileboard-record" key={index}> | ||
| 317 | {record.scores.map((e, i) => (<> | ||
| 318 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | ||
| 319 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | ||
| 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> | ||
| 322 | <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> | ||
| 323 | <span> </span> | 435 | <span> </span> |
| 324 | {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} | 436 | )} |
| 325 | <span>{record!.scores[i].date.split("T")[0]}</span> | 437 | <span>{e.date.split('T')[0]}</span> |
| 326 | <span style={{ flexDirection: "row-reverse" }}> | 438 | <span style={{ flexDirection: 'row-reverse' }}> |
| 327 | 439 | <button | |
| 328 | <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | 440 | style={{ marginRight: '10px' }} |
| 329 | <button onClick={() => { _delete_submission(r.id, e.record_id) }}><img src={DeleteIcon}></img></button> | 441 | onClick={() => { |
| 330 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | 442 | message( |
| 331 | {i === 0 && record!.scores.length > 1 ? <button onClick={() => { | 443 | 'Demo Information', |
| 332 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | 444 | `Demo ID: ${e.demo_id}` |
| 333 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | 445 | ); |
| 334 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : | 446 | }} |
| 335 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | 447 | > |
| 448 | <img src={ThreedotIcon} alt="demo_id" /> | ||
| 449 | </button> | ||
| 450 | <button | ||
| 451 | onClick={() => { | ||
| 452 | _delete_submission(r.map_id, e.record_id); | ||
| 453 | }} | ||
| 454 | > | ||
| 455 | <img src={DeleteIcon}></img> | ||
| 456 | </button> | ||
| 457 | <button | ||
| 458 | onClick={() => | ||
| 459 | (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`) | ||
| 336 | } | 460 | } |
| 337 | }><img src={HistoryIcon} alt="history" /></button> : ""} | 461 | > |
| 338 | 462 | <img src={DownloadIcon} alt="download" /> | |
| 463 | </button> | ||
| 464 | {i === 0 && r.scores.length > 1 ? ( | ||
| 465 | <button | ||
| 466 | onClick={() => { | ||
| 467 | ( | ||
| 468 | document.querySelectorAll( | ||
| 469 | '.profileboard-record' | ||
| 470 | )[index % 20] as HTMLInputElement | ||
| 471 | ).style.height === '44px' || | ||
| 472 | ( | ||
| 473 | document.querySelectorAll( | ||
| 474 | '.profileboard-record' | ||
| 475 | )[index % 20] as HTMLInputElement | ||
| 476 | ).style.height === '' | ||
| 477 | ? (( | ||
| 478 | document.querySelectorAll( | ||
| 479 | '.profileboard-record' | ||
| 480 | )[index % 20] as HTMLInputElement | ||
| 481 | ).style.height = | ||
| 482 | `${r.scores.length * 46}px`) | ||
| 483 | : (( | ||
| 484 | document.querySelectorAll( | ||
| 485 | '.profileboard-record' | ||
| 486 | )[index % 20] as HTMLInputElement | ||
| 487 | ).style.height = '44px'); | ||
| 488 | }} | ||
| 489 | > | ||
| 490 | <img src={HistoryIcon} alt="history" /> | ||
| 491 | </button> | ||
| 492 | ) : ( | ||
| 493 | '' | ||
| 494 | )} | ||
| 495 | </span> | ||
| 496 | </> | ||
| 497 | ))} | ||
| 498 | </button> | ||
| 499 | ) : ( | ||
| 500 | '' | ||
| 501 | ) | ||
| 502 | ) | ||
| 503 | ) : maps ? ( | ||
| 504 | maps | ||
| 505 | .filter(e => e.is_disabled === false) | ||
| 506 | .sort((a, b) => a.id - b.id) | ||
| 507 | .map((r, index) => { | ||
| 508 | if (Math.ceil((index + 1) / 20) === pageNumber) { | ||
| 509 | let record = profile.records.find(e => e.map_id === r.id); | ||
| 510 | return record === undefined ? ( | ||
| 511 | <button | ||
| 512 | className="profileboard-record" | ||
| 513 | key={index} | ||
| 514 | style={{ backgroundColor: '#1b1b20' }} | ||
| 515 | > | ||
| 516 | <Link to={`/maps/${r.id}`}> | ||
| 517 | <span>{r.name}</span> | ||
| 518 | </Link> | ||
| 519 | <span style={{ display: 'grid' }}>N/A</span> | ||
| 520 | <span style={{ display: 'grid' }}>N/A</span> | ||
| 521 | <span>N/A</span> | ||
| 522 | <span> </span> | ||
| 523 | <span>N/A</span> | ||
| 524 | <span>N/A</span> | ||
| 525 | <span style={{ flexDirection: 'row-reverse' }}></span> | ||
| 526 | </button> | ||
| 527 | ) : ( | ||
| 528 | <button className="profileboard-record" key={index}> | ||
| 529 | {record.scores.map((e, i) => ( | ||
| 530 | <> | ||
| 531 | {i !== 0 ? ( | ||
| 532 | <hr style={{ gridColumn: '1 / span 8' }} /> | ||
| 533 | ) : ( | ||
| 534 | '' | ||
| 535 | )} | ||
| 536 | <Link to={`/maps/${r.id}`}> | ||
| 537 | <span>{r.name}</span> | ||
| 538 | </Link> | ||
| 539 | <span style={{ display: 'grid' }}> | ||
| 540 | {record!.scores[i].score_count} | ||
| 339 | </span> | 541 | </span> |
| 340 | </>))} | 542 | <span style={{ display: 'grid' }}> |
| 341 | </button> | 543 | {record!.scores[i].score_count - |
| 342 | 544 | record!.map_wr_count > | |
| 343 | ) | 545 | 0 |
| 344 | } else { return null } | 546 | ? `+${record!.scores[i].score_count - record!.map_wr_count}` |
| 345 | }) : (<>{console.warn(maps)}</>)} | 547 | : `-`} |
| 548 | </span> | ||
| 549 | <span style={{ display: 'grid' }}> | ||
| 550 | {ticks_to_time(record!.scores[i].score_time)} | ||
| 551 | </span> | ||
| 552 | <span> </span> | ||
| 553 | {i === 0 ? ( | ||
| 554 | <span>#{record!.placement}</span> | ||
| 555 | ) : ( | ||
| 556 | <span> </span> | ||
| 557 | )} | ||
| 558 | <span>{record!.scores[i].date.split('T')[0]}</span> | ||
| 559 | <span style={{ flexDirection: 'row-reverse' }}> | ||
| 560 | <button | ||
| 561 | onClick={() => { | ||
| 562 | message( | ||
| 563 | 'Demo Information', | ||
| 564 | `Demo ID: ${e.demo_id}` | ||
| 565 | ); | ||
| 566 | }} | ||
| 567 | > | ||
| 568 | <img src={ThreedotIcon} alt="demo_id" /> | ||
| 569 | </button> | ||
| 570 | <button | ||
| 571 | onClick={() => { | ||
| 572 | _delete_submission(r.id, e.record_id); | ||
| 573 | }} | ||
| 574 | > | ||
| 575 | <img src={DeleteIcon}></img> | ||
| 576 | </button> | ||
| 577 | <button | ||
| 578 | onClick={() => | ||
| 579 | (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`) | ||
| 580 | } | ||
| 581 | > | ||
| 582 | <img src={DownloadIcon} alt="download" /> | ||
| 583 | </button> | ||
| 584 | {i === 0 && record!.scores.length > 1 ? ( | ||
| 585 | <button | ||
| 586 | onClick={() => { | ||
| 587 | ( | ||
| 588 | document.querySelectorAll( | ||
| 589 | '.profileboard-record' | ||
| 590 | )[index % 20] as HTMLInputElement | ||
| 591 | ).style.height === '44px' || | ||
| 592 | ( | ||
| 593 | document.querySelectorAll( | ||
| 594 | '.profileboard-record' | ||
| 595 | )[index % 20] as HTMLInputElement | ||
| 596 | ).style.height === '' | ||
| 597 | ? (( | ||
| 598 | document.querySelectorAll( | ||
| 599 | '.profileboard-record' | ||
| 600 | )[index % 20] as HTMLInputElement | ||
| 601 | ).style.height = | ||
| 602 | `${record!.scores.length * 46}px`) | ||
| 603 | : (( | ||
| 604 | document.querySelectorAll( | ||
| 605 | '.profileboard-record' | ||
| 606 | )[index % 20] as HTMLInputElement | ||
| 607 | ).style.height = '44px'); | ||
| 608 | }} | ||
| 609 | > | ||
| 610 | <img src={HistoryIcon} alt="history" /> | ||
| 611 | </button> | ||
| 612 | ) : ( | ||
| 613 | '' | ||
| 614 | )} | ||
| 615 | </span> | ||
| 616 | </> | ||
| 617 | ))} | ||
| 618 | </button> | ||
| 619 | ); | ||
| 620 | } else { | ||
| 621 | return null; | ||
| 622 | } | ||
| 623 | }) | ||
| 624 | ) : ( | ||
| 625 | <>{console.warn(maps)}</> | ||
| 626 | )} | ||
| 346 | </div> | 627 | </div> |
| 347 | </section> | 628 | </section> |
| 348 | </main> | 629 | </main> |
diff --git a/frontend/src/pages/Rankings.tsx b/frontend/src/pages/Rankings.tsx index 71aa427..885638d 100644 --- a/frontend/src/pages/Rankings.tsx +++ b/frontend/src/pages/Rankings.tsx | |||
| @@ -1,147 +1,200 @@ | |||
| 1 | import React, { useEffect } from "react"; | 1 | import React, { useEffect } from 'react'; |
| 2 | import { Helmet } from "react-helmet"; | 2 | import { Helmet } from 'react-helmet'; |
| 3 | 3 | ||
| 4 | import RankingEntry from "@components/RankingEntry"; | 4 | import RankingEntry from '@components/RankingEntry'; |
| 5 | import { Ranking, SteamRanking, RankingType, SteamRankingType } from "@customTypes/Ranking"; | 5 | import { |
| 6 | import { API } from "@api/Api"; | 6 | Ranking, |
| 7 | SteamRanking, | ||
| 8 | RankingType, | ||
| 9 | SteamRankingType, | ||
| 10 | } from '@customTypes/Ranking'; | ||
| 11 | import { API } from '@api/Api'; | ||
| 7 | 12 | ||
| 8 | import "@css/Rankings.css"; | 13 | import '@css/Rankings.css'; |
| 9 | 14 | ||
| 10 | const Rankings: React.FC = () => { | 15 | const Rankings: React.FC = () => { |
| 11 | const [leaderboardData, setLeaderboardData] = React.useState<Ranking | SteamRanking>(); | 16 | const [leaderboardData, setLeaderboardData] = React.useState< |
| 12 | const [currentLeaderboard, setCurrentLeaderboard] = React.useState<RankingType[] | SteamRankingType[]>(); | 17 | Ranking | SteamRanking |
| 13 | enum LeaderboardTypes { | 18 | >(); |
| 14 | official, | 19 | const [currentLeaderboard, setCurrentLeaderboard] = React.useState< |
| 15 | unofficial | 20 | RankingType[] | SteamRankingType[] |
| 21 | >(); | ||
| 22 | enum LeaderboardTypes { | ||
| 23 | official, | ||
| 24 | unofficial, | ||
| 25 | } | ||
| 26 | const [currentRankingType, setCurrentRankingType] = | ||
| 27 | React.useState<LeaderboardTypes>(LeaderboardTypes.official); | ||
| 28 | |||
| 29 | const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false); | ||
| 30 | |||
| 31 | enum RankingCategories { | ||
| 32 | rankings_overall, | ||
| 33 | rankings_multiplayer, | ||
| 34 | rankings_singleplayer, | ||
| 35 | } | ||
| 36 | const [currentLeaderboardType, setCurrentLeaderboardType] = | ||
| 37 | React.useState<RankingCategories>(RankingCategories.rankings_singleplayer); | ||
| 38 | const [load, setLoad] = React.useState<boolean>(false); | ||
| 39 | |||
| 40 | const _fetch_rankings = async () => { | ||
| 41 | setLeaderboardLoad(false); | ||
| 42 | const rankings = await API.get_official_rankings(); | ||
| 43 | setLeaderboardData(rankings); | ||
| 44 | if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { | ||
| 45 | setCurrentLeaderboard(rankings.rankings_singleplayer); | ||
| 46 | } else if ( | ||
| 47 | currentLeaderboardType == RankingCategories.rankings_multiplayer | ||
| 48 | ) { | ||
| 49 | setCurrentLeaderboard(rankings.rankings_multiplayer); | ||
| 50 | } else { | ||
| 51 | setCurrentLeaderboard(rankings.rankings_overall); | ||
| 16 | } | 52 | } |
| 17 | const [currentRankingType, setCurrentRankingType] = React.useState<LeaderboardTypes>(LeaderboardTypes.official); | 53 | setLoad(true); |
| 18 | 54 | setLeaderboardLoad(true); | |
| 19 | const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false); | 55 | }; |
| 20 | 56 | ||
| 21 | enum RankingCategories { | 57 | const __dev_fetch_unofficial_rankings = async () => { |
| 22 | rankings_overall, | 58 | try { |
| 23 | rankings_multiplayer, | 59 | setLeaderboardLoad(false); |
| 24 | rankings_singleplayer | 60 | const rankings = await API.get_unofficial_rankings(); |
| 25 | } | 61 | setLeaderboardData(rankings); |
| 26 | const [currentLeaderboardType, setCurrentLeaderboardType] = React.useState<RankingCategories>(RankingCategories.rankings_singleplayer); | 62 | if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { |
| 27 | const [load, setLoad] = React.useState<boolean>(false); | 63 | // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) |
| 28 | 64 | setCurrentLeaderboard(rankings.rankings_singleplayer); | |
| 29 | const _fetch_rankings = async () => { | 65 | } else if ( |
| 30 | setLeaderboardLoad(false); | 66 | currentLeaderboardType == RankingCategories.rankings_multiplayer |
| 31 | const rankings = await API.get_official_rankings(); | 67 | ) { |
| 32 | setLeaderboardData(rankings); | 68 | setCurrentLeaderboard(rankings.rankings_multiplayer); |
| 33 | if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { | 69 | } else { |
| 34 | setCurrentLeaderboard(rankings.rankings_singleplayer) | 70 | setCurrentLeaderboard(rankings.rankings_overall); |
| 35 | } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { | 71 | } |
| 36 | setCurrentLeaderboard(rankings.rankings_multiplayer) | 72 | setLeaderboardLoad(true); |
| 37 | } else { | 73 | } catch (e) { |
| 38 | setCurrentLeaderboard(rankings.rankings_overall) | 74 | console.log(e); |
| 39 | } | ||
| 40 | setLoad(true); | ||
| 41 | setLeaderboardLoad(true); | ||
| 42 | } | 75 | } |
| 43 | 76 | }; | |
| 44 | const __dev_fetch_unofficial_rankings = async () => { | 77 | |
| 45 | try { | 78 | const _set_current_leaderboard = (ranking_cat: RankingCategories) => { |
| 46 | setLeaderboardLoad(false); | 79 | if (ranking_cat == RankingCategories.rankings_singleplayer) { |
| 47 | const rankings = await API.get_unofficial_rankings(); | 80 | setCurrentLeaderboard(leaderboardData!.rankings_singleplayer); |
| 48 | setLeaderboardData(rankings); | 81 | } else if (ranking_cat == RankingCategories.rankings_multiplayer) { |
| 49 | if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { | 82 | setCurrentLeaderboard(leaderboardData!.rankings_multiplayer); |
| 50 | // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) | 83 | } else { |
| 51 | setCurrentLeaderboard(rankings.rankings_singleplayer) | 84 | setCurrentLeaderboard(leaderboardData!.rankings_overall); |
| 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 | } | 85 | } |
| 62 | 86 | ||
| 63 | const _set_current_leaderboard = (ranking_cat: RankingCategories) => { | 87 | setCurrentLeaderboardType(ranking_cat); |
| 64 | if (ranking_cat == RankingCategories.rankings_singleplayer) { | 88 | }; |
| 65 | setCurrentLeaderboard(leaderboardData!.rankings_singleplayer); | ||
| 66 | } else if (ranking_cat == RankingCategories.rankings_multiplayer) { | ||
| 67 | setCurrentLeaderboard(leaderboardData!.rankings_multiplayer); | ||
| 68 | } else { | ||
| 69 | setCurrentLeaderboard(leaderboardData!.rankings_overall); | ||
| 70 | } | ||
| 71 | 89 | ||
| 72 | setCurrentLeaderboardType(ranking_cat); | 90 | const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => { |
| 91 | if (leaderboard_type == LeaderboardTypes.official) { | ||
| 92 | _fetch_rankings(); | ||
| 93 | } else { | ||
| 73 | } | 94 | } |
| 95 | }; | ||
| 74 | 96 | ||
| 75 | const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => { | 97 | useEffect(() => { |
| 76 | if (leaderboard_type == LeaderboardTypes.official) { | 98 | _fetch_rankings(); |
| 77 | _fetch_rankings(); | 99 | if (load) { |
| 78 | } else { | 100 | _set_current_leaderboard(RankingCategories.rankings_singleplayer); |
| 79 | |||
| 80 | } | ||
| 81 | } | 101 | } |
| 82 | 102 | }, [load]); | |
| 83 | useEffect(() => { | 103 | |
| 84 | _fetch_rankings(); | 104 | return ( |
| 85 | if (load) { | 105 | <main> |
| 86 | _set_current_leaderboard(RankingCategories.rankings_singleplayer); | 106 | <Helmet> |
| 87 | } | 107 | <title>LPHUB | Rankings</title> |
| 88 | }, [load]) | 108 | </Helmet> |
| 89 | 109 | <section className="nav-container nav-1"> | |
| 90 | return ( | 110 | <div> |
| 91 | <main> | 111 | <button |
| 92 | <Helmet> | 112 | onClick={() => { |
| 93 | <title>LPHUB | Rankings</title> | 113 | _fetch_rankings(); |
| 94 | </Helmet> | 114 | setCurrentRankingType(LeaderboardTypes.official); |
| 95 | <section className="nav-container nav-1"> | 115 | }} |
| 96 | <div> | 116 | className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? 'selected' : ''}`} |
| 97 | <button onClick={() => { _fetch_rankings(); setCurrentRankingType(LeaderboardTypes.official) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? "selected" : ""}`}> | 117 | > |
| 98 | <span>Official (LPHUB)</span> | 118 | <span>Official (LPHUB)</span> |
| 99 | </button> | 119 | </button> |
| 100 | <button onClick={() => { __dev_fetch_unofficial_rankings(); setCurrentRankingType(LeaderboardTypes.unofficial) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? "selected" : ""}`}> | 120 | <button |
| 101 | <span>Unofficial (Steam)</span> | 121 | onClick={() => { |
| 102 | </button> | 122 | __dev_fetch_unofficial_rankings(); |
| 103 | </div> | 123 | setCurrentRankingType(LeaderboardTypes.unofficial); |
| 104 | </section> | 124 | }} |
| 105 | <section className="nav-container nav-2"> | 125 | className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? 'selected' : ''}`} |
| 106 | <div> | 126 | > |
| 107 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_singleplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_singleplayer ? "selected" : ""}`}> | 127 | <span>Unofficial (Steam)</span> |
| 108 | <span>Singleplayer</span> | 128 | </button> |
| 109 | </button> | 129 | </div> |
| 110 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_multiplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_multiplayer ? "selected" : ""}`}> | 130 | </section> |
| 111 | <span>Cooperative</span> | 131 | <section className="nav-container nav-2"> |
| 112 | </button> | 132 | <div> |
| 113 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_overall)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_overall ? "selected" : ""}`}> | 133 | <button |
| 114 | <span>Overall</span> | 134 | onClick={() => |
| 115 | </button> | 135 | _set_current_leaderboard(RankingCategories.rankings_singleplayer) |
| 116 | </div> | 136 | } |
| 117 | </section> | 137 | className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_singleplayer ? 'selected' : ''}`} |
| 118 | 138 | > | |
| 119 | {load ? | 139 | <span>Singleplayer</span> |
| 120 | <section className="rankings-leaderboard"> | 140 | </button> |
| 121 | <div className="ranks-container"> | 141 | <button |
| 122 | <div className="leaderboard-entry header"> | 142 | onClick={() => |
| 123 | <span>Rank</span> | 143 | _set_current_leaderboard(RankingCategories.rankings_multiplayer) |
| 124 | <span>Player</span> | 144 | } |
| 125 | <span>Portals</span> | 145 | className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_multiplayer ? 'selected' : ''}`} |
| 126 | </div> | 146 | > |
| 127 | 147 | <span>Cooperative</span> | |
| 128 | <div className="splitter"></div> | 148 | </button> |
| 129 | 149 | <button | |
| 130 | {leaderboardLoad && currentLeaderboard?.map((curRankingData, i) => { | 150 | onClick={() => |
| 131 | return <RankingEntry currentLeaderboardType={currentLeaderboardType} curRankingData={curRankingData} key={i}></RankingEntry> | 151 | _set_current_leaderboard(RankingCategories.rankings_overall) |
| 132 | }) | 152 | } |
| 133 | } | 153 | className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_overall ? 'selected' : ''}`} |
| 134 | 154 | > | |
| 135 | {leaderboardLoad ? null : | 155 | <span>Overall</span> |
| 136 | <div style={{ display: "flex", justifyContent: "center", margin: "30px 0px" }}> | 156 | </button> |
| 137 | <span className="loader"></span> | 157 | </div> |
| 138 | </div> | 158 | </section> |
| 139 | } | 159 | |
| 140 | </div> | 160 | {load ? ( |
| 141 | </section> | 161 | <section className="rankings-leaderboard"> |
| 142 | : null} | 162 | <div className="ranks-container"> |
| 143 | </main> | 163 | <div className="leaderboard-entry header"> |
| 144 | ) | 164 | <span>Rank</span> |
| 145 | } | 165 | <span>Player</span> |
| 166 | <span>Portals</span> | ||
| 167 | </div> | ||
| 168 | |||
| 169 | <div className="splitter"></div> | ||
| 170 | |||
| 171 | {leaderboardLoad && | ||
| 172 | currentLeaderboard?.map((curRankingData, i) => { | ||
| 173 | return ( | ||
| 174 | <RankingEntry | ||
| 175 | currentLeaderboardType={currentLeaderboardType} | ||
| 176 | curRankingData={curRankingData} | ||
| 177 | key={i} | ||
| 178 | ></RankingEntry> | ||
| 179 | ); | ||
| 180 | })} | ||
| 181 | |||
| 182 | {leaderboardLoad ? null : ( | ||
| 183 | <div | ||
| 184 | style={{ | ||
| 185 | display: 'flex', | ||
| 186 | justifyContent: 'center', | ||
| 187 | margin: '30px 0px', | ||
| 188 | }} | ||
| 189 | > | ||
| 190 | <span className="loader"></span> | ||
| 191 | </div> | ||
| 192 | )} | ||
| 193 | </div> | ||
| 194 | </section> | ||
| 195 | ) : null} | ||
| 196 | </main> | ||
| 197 | ); | ||
| 198 | }; | ||
| 146 | 199 | ||
| 147 | export default Rankings; | 200 | export default Rankings; |
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx index 9f57b7e..7a774bc 100644 --- a/frontend/src/pages/Rules.tsx +++ b/frontend/src/pages/Rules.tsx | |||
| @@ -5,37 +5,35 @@ import { Helmet } from 'react-helmet'; | |||
| 5 | import '@css/Rules.css'; | 5 | import '@css/Rules.css'; |
| 6 | 6 | ||
| 7 | const Rules: React.FC = () => { | 7 | const Rules: React.FC = () => { |
| 8 | const [rulesText, setRulesText] = React.useState<string>(''); | ||
| 8 | 9 | ||
| 9 | const [rulesText, setRulesText] = React.useState<string>(""); | 10 | React.useEffect(() => { |
| 11 | const fetchRules = async () => { | ||
| 12 | try { | ||
| 13 | const response = await fetch( | ||
| 14 | 'https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md' | ||
| 15 | ); | ||
| 16 | if (!response.ok) { | ||
| 17 | throw new Error('Failed to fetch README'); | ||
| 18 | } | ||
| 19 | const rulesText = await response.text(); | ||
| 20 | setRulesText(rulesText); | ||
| 21 | } catch (error) { | ||
| 22 | console.error('Error fetching Rules:', error); | ||
| 23 | } | ||
| 24 | // setRulesText(rulesText) | ||
| 25 | }; | ||
| 26 | fetchRules(); | ||
| 27 | }, []); | ||
| 10 | 28 | ||
| 11 | React.useEffect(() => { | 29 | return ( |
| 12 | const fetchRules = async () => { | 30 | <main> |
| 13 | try { | 31 | <Helmet> |
| 14 | const response = await fetch( | 32 | <title>LPHUB | Rules</title> |
| 15 | 'https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md' | 33 | </Helmet> |
| 16 | ); | 34 | <ReactMarkdown>{rulesText}</ReactMarkdown> |
| 17 | if (!response.ok) { | 35 | </main> |
| 18 | throw new Error('Failed to fetch README'); | 36 | ); |
| 19 | } | ||
| 20 | const rulesText = await response.text(); | ||
| 21 | setRulesText(rulesText); | ||
| 22 | } catch (error) { | ||
| 23 | console.error('Error fetching Rules:', error); | ||
| 24 | } | ||
| 25 | // setRulesText(rulesText) | ||
| 26 | }; | ||
| 27 | fetchRules(); | ||
| 28 | }, []); | ||
| 29 | |||
| 30 | |||
| 31 | return ( | ||
| 32 | <main> | ||
| 33 | <Helmet> | ||
| 34 | <title>LPHUB | Rules</title> | ||
| 35 | </Helmet> | ||
| 36 | <ReactMarkdown>{rulesText}</ReactMarkdown> | ||
| 37 | </main> | ||
| 38 | ); | ||
| 39 | }; | 37 | }; |
| 40 | 38 | ||
| 41 | export default Rules; | 39 | export default Rules; |
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx index d43c0c6..b6dfbd3 100644 --- a/frontend/src/pages/User.tsx +++ b/frontend/src/pages/User.tsx | |||
| @@ -2,13 +2,24 @@ 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 { |
| 6 | SteamIcon, | ||
| 7 | TwitchIcon, | ||
| 8 | YouTubeIcon, | ||
| 9 | PortalIcon, | ||
| 10 | FlagIcon, | ||
| 11 | StatisticsIcon, | ||
| 12 | SortIcon, | ||
| 13 | ThreedotIcon, | ||
| 14 | DownloadIcon, | ||
| 15 | HistoryIcon, | ||
| 16 | } from '@images/Images'; | ||
| 6 | import { UserProfile } from '@customTypes/Profile'; | 17 | import { UserProfile } from '@customTypes/Profile'; |
| 7 | import { Game, GameChapters } from '@customTypes/Game'; | 18 | import { Game, GameChapters } from '@customTypes/Game'; |
| 8 | import { Map } from '@customTypes/Map'; | 19 | import { Map } from '@customTypes/Map'; |
| 9 | import { API } from '@api/Api'; | 20 | import { API } from '@api/Api'; |
| 10 | import { ticks_to_time } from '@utils/Time'; | 21 | import { ticks_to_time } from '@utils/Time'; |
| 11 | import "@css/Profile.css"; | 22 | import '@css/Profile.css'; |
| 12 | import useMessage from '@hooks/UseMessage'; | 23 | import useMessage from '@hooks/UseMessage'; |
| 13 | 24 | ||
| 14 | interface UserProps { | 25 | interface UserProps { |
| @@ -18,7 +29,6 @@ interface UserProps { | |||
| 18 | } | 29 | } |
| 19 | 30 | ||
| 20 | const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | 31 | const User: React.FC<UserProps> = ({ token, profile, gameData }) => { |
| 21 | |||
| 22 | const { message, MessageDialogComponent } = useMessage(); | 32 | const { message, MessageDialogComponent } = useMessage(); |
| 23 | 33 | ||
| 24 | const [user, setUser] = React.useState<UserProfile | undefined>(undefined); | 34 | const [user, setUser] = React.useState<UserProfile | undefined>(undefined); |
| @@ -27,18 +37,20 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 27 | const [pageNumber, setPageNumber] = React.useState(1); | 37 | const [pageNumber, setPageNumber] = React.useState(1); |
| 28 | const [pageMax, setPageMax] = React.useState(0); | 38 | const [pageMax, setPageMax] = React.useState(0); |
| 29 | 39 | ||
| 30 | const [game, setGame] = React.useState("0"); | 40 | const [game, setGame] = React.useState('0'); |
| 31 | const [chapter, setChapter] = React.useState("0"); | 41 | const [chapter, setChapter] = React.useState('0'); |
| 32 | const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); | 42 | const [chapterData, setChapterData] = React.useState<GameChapters | null>( |
| 43 | null | ||
| 44 | ); | ||
| 33 | const [maps, setMaps] = React.useState<Map[]>([]); | 45 | const [maps, setMaps] = React.useState<Map[]>([]); |
| 34 | 46 | ||
| 35 | const location = useLocation(); | 47 | const location = useLocation(); |
| 36 | const navigate = useNavigate(); | 48 | const navigate = useNavigate(); |
| 37 | 49 | ||
| 38 | const _fetch_user = async () => { | 50 | const _fetch_user = async () => { |
| 39 | const userID = location.pathname.split("/")[2]; | 51 | const userID = location.pathname.split('/')[2]; |
| 40 | if (token && profile && profile.profile && profile.steam_id === userID) { | 52 | if (token && profile && profile.profile && profile.steam_id === userID) { |
| 41 | navigate("/profile"); | 53 | navigate('/profile'); |
| 42 | return; | 54 | return; |
| 43 | } | 55 | } |
| 44 | const userData = await API.get_user(userID); | 56 | const userData = await API.get_user(userID); |
| @@ -46,7 +58,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 46 | }; | 58 | }; |
| 47 | 59 | ||
| 48 | const _get_game_chapters = async () => { | 60 | const _get_game_chapters = async () => { |
| 49 | if (game !== "0") { | 61 | if (game !== '0') { |
| 50 | const gameChapters = await API.get_games_chapters(game); | 62 | const gameChapters = await API.get_games_chapters(game); |
| 51 | setChapterData(gameChapters); | 63 | setChapterData(gameChapters); |
| 52 | } else { | 64 | } else { |
| @@ -56,7 +68,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 56 | }; | 68 | }; |
| 57 | 69 | ||
| 58 | const _get_game_maps = async () => { | 70 | const _get_game_maps = async () => { |
| 59 | if (chapter === "0") { | 71 | if (chapter === '0') { |
| 60 | const gameMaps = await API.get_game_maps(game); | 72 | const gameMaps = await API.get_game_maps(game); |
| 61 | setMaps(gameMaps); | 73 | setMaps(gameMaps); |
| 62 | setPageMax(Math.ceil(gameMaps.length / 20)); | 74 | setPageMax(Math.ceil(gameMaps.length / 20)); |
| @@ -80,16 +92,14 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 80 | }, [user, game, location]); | 92 | }, [user, game, location]); |
| 81 | 93 | ||
| 82 | React.useEffect(() => { | 94 | React.useEffect(() => { |
| 83 | if (user && game !== "0") { | 95 | if (user && game !== '0') { |
| 84 | _get_game_maps(); | 96 | _get_game_maps(); |
| 85 | } | 97 | } |
| 86 | }, [user, game, chapter, location]) | 98 | }, [user, game, chapter, location]); |
| 87 | 99 | ||
| 88 | if (!user) { | 100 | if (!user) { |
| 89 | return ( | 101 | return <></>; |
| 90 | <></> | 102 | } |
| 91 | ); | ||
| 92 | }; | ||
| 93 | 103 | ||
| 94 | return ( | 104 | return ( |
| 95 | <main> | 105 | <main> |
| @@ -98,218 +108,461 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 98 | <meta name="description" content={user.user_name} /> | 108 | <meta name="description" content={user.user_name} /> |
| 99 | </Helmet> | 109 | </Helmet> |
| 100 | {MessageDialogComponent} | 110 | {MessageDialogComponent} |
| 101 | <section id='section1' className='profile'> | 111 | <section id="section1" className="profile"> |
| 102 | <div> | 112 | <div> |
| 103 | <img src={user.avatar_link} alt="profile-image"></img> | 113 | <img src={user.avatar_link} alt="profile-image"></img> |
| 104 | </div> | 114 | </div> |
| 105 | <div id='profile-top'> | 115 | <div id="profile-top"> |
| 106 | <div> | 116 | <div> |
| 107 | <div>{user.user_name}</div> | 117 | <div>{user.user_name}</div> |
| 108 | <div> | 118 | <div> |
| 109 | {user.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`} alt={user.country_code} />} | 119 | {user.country_code === 'XX' ? ( |
| 120 | '' | ||
| 121 | ) : ( | ||
| 122 | <img | ||
| 123 | src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`} | ||
| 124 | alt={user.country_code} | ||
| 125 | /> | ||
| 126 | )} | ||
| 110 | </div> | 127 | </div> |
| 111 | <div> | 128 | <div> |
| 112 | {user.titles.map(e => ( | 129 | {user.titles.map(e => ( |
| 113 | <span className="titles" style={{ backgroundColor: `#${e.color}` }}> | 130 | <span |
| 131 | className="titles" | ||
| 132 | style={{ backgroundColor: `#${e.color}` }} | ||
| 133 | > | ||
| 114 | {e.name} | 134 | {e.name} |
| 115 | </span> | 135 | </span> |
| 116 | ))} | 136 | ))} |
| 117 | </div> | 137 | </div> |
| 118 | </div> | 138 | </div> |
| 119 | <div> | 139 | <div> |
| 120 | {user.links.steam === "-" ? "" : <a href={user.links.steam}><img src={SteamIcon} alt="Steam" /></a>} | 140 | {user.links.steam === '-' ? ( |
| 121 | {user.links.twitch === "-" ? "" : <a href={user.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} | 141 | '' |
| 122 | {user.links.youtube === "-" ? "" : <a href={user.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} | 142 | ) : ( |
| 123 | {user.links.p2sr === "-" ? "" : <a href={user.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} | 143 | <a href={user.links.steam}> |
| 144 | <img src={SteamIcon} alt="Steam" /> | ||
| 145 | </a> | ||
| 146 | )} | ||
| 147 | {user.links.twitch === '-' ? ( | ||
| 148 | '' | ||
| 149 | ) : ( | ||
| 150 | <a href={user.links.twitch}> | ||
| 151 | <img src={TwitchIcon} alt="Twitch" /> | ||
| 152 | </a> | ||
| 153 | )} | ||
| 154 | {user.links.youtube === '-' ? ( | ||
| 155 | '' | ||
| 156 | ) : ( | ||
| 157 | <a href={user.links.youtube}> | ||
| 158 | <img src={YouTubeIcon} alt="Youtube" /> | ||
| 159 | </a> | ||
| 160 | )} | ||
| 161 | {user.links.p2sr === '-' ? ( | ||
| 162 | '' | ||
| 163 | ) : ( | ||
| 164 | <a href={user.links.p2sr}> | ||
| 165 | <img src={PortalIcon} alt="P2SR" style={{ padding: '0' }} /> | ||
| 166 | </a> | ||
| 167 | )} | ||
| 124 | </div> | 168 | </div> |
| 125 | |||
| 126 | </div> | 169 | </div> |
| 127 | <div id='profile-bottom'> | 170 | <div id="profile-bottom"> |
| 128 | <div> | 171 | <div> |
| 129 | <span>Overall</span> | 172 | <span>Overall</span> |
| 130 | <span>{user.rankings.overall.rank === 0 ? "N/A " : "#" + user.rankings.overall.rank + " "} | 173 | <span> |
| 131 | <span>({user.rankings.overall.completion_count}/{user.rankings.overall.completion_total})</span> | 174 | {user.rankings.overall.rank === 0 |
| 175 | ? 'N/A ' | ||
| 176 | : '#' + user.rankings.overall.rank + ' '} | ||
| 177 | <span> | ||
| 178 | ({user.rankings.overall.completion_count}/ | ||
| 179 | {user.rankings.overall.completion_total}) | ||
| 180 | </span> | ||
| 132 | </span> | 181 | </span> |
| 133 | </div> | 182 | </div> |
| 134 | <div> | 183 | <div> |
| 135 | <span>Singleplayer</span> | 184 | <span>Singleplayer</span> |
| 136 | <span>{user.rankings.singleplayer.rank === 0 ? "N/A " : "#" + user.rankings.singleplayer.rank + " "} | 185 | <span> |
| 137 | <span>({user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total})</span> | 186 | {user.rankings.singleplayer.rank === 0 |
| 187 | ? 'N/A ' | ||
| 188 | : '#' + user.rankings.singleplayer.rank + ' '} | ||
| 189 | <span> | ||
| 190 | ({user.rankings.singleplayer.completion_count}/ | ||
| 191 | {user.rankings.singleplayer.completion_total}) | ||
| 192 | </span> | ||
| 138 | </span> | 193 | </span> |
| 139 | </div> | 194 | </div> |
| 140 | <div> | 195 | <div> |
| 141 | <span>Cooperative</span> | 196 | <span>Cooperative</span> |
| 142 | <span>{user.rankings.cooperative.rank === 0 ? "N/A " : "#" + user.rankings.cooperative.rank + " "} | 197 | <span> |
| 143 | <span>({user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total})</span> | 198 | {user.rankings.cooperative.rank === 0 |
| 199 | ? 'N/A ' | ||
| 200 | : '#' + user.rankings.cooperative.rank + ' '} | ||
| 201 | <span> | ||
| 202 | ({user.rankings.cooperative.completion_count}/ | ||
| 203 | {user.rankings.cooperative.completion_total}) | ||
| 204 | </span> | ||
| 144 | </span> | 205 | </span> |
| 145 | </div> | 206 | </div> |
| 146 | </div> | 207 | </div> |
| 147 | </section> | 208 | </section> |
| 148 | 209 | ||
| 149 | 210 | <section id="section2" className="profile"> | |
| 150 | <section id='section2' className='profile'> | 211 | <button onClick={() => setNavState(0)}> |
| 151 | <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" /> Player Records</button> | 212 | <img src={FlagIcon} alt="" /> |
| 152 | <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" /> Statistics</button> | 213 | Player Records |
| 214 | </button> | ||
| 215 | <button onClick={() => setNavState(1)}> | ||
| 216 | <img src={StatisticsIcon} alt="" /> | ||
| 217 | Statistics | ||
| 218 | </button> | ||
| 153 | </section> | 219 | </section> |
| 154 | 220 | ||
| 155 | 221 | <section id="section3" className="profile1"> | |
| 156 | 222 | <div id="profileboard-nav"> | |
| 157 | 223 | {gameData === null ? ( | |
| 158 | 224 | <select>error</select> | |
| 159 | <section id='section3' className='profile1'> | 225 | ) : ( |
| 160 | <div id='profileboard-nav'> | 226 | <select |
| 161 | {gameData === null ? <select>error</select> : | 227 | id="select-game" |
| 162 | |||
| 163 | <select id='select-game' | ||
| 164 | onChange={() => { | 228 | onChange={() => { |
| 165 | setGame((document.querySelector('#select-game') as HTMLInputElement).value); | 229 | setGame( |
| 166 | setChapter("0"); | 230 | (document.querySelector('#select-game') as HTMLInputElement) |
| 167 | const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; | 231 | .value |
| 232 | ); | ||
| 233 | setChapter('0'); | ||
| 234 | const chapterSelect = document.querySelector( | ||
| 235 | '#select-chapter' | ||
| 236 | ) as HTMLSelectElement; | ||
| 168 | if (chapterSelect) { | 237 | if (chapterSelect) { |
| 169 | chapterSelect.value = "0"; | 238 | chapterSelect.value = '0'; |
| 170 | } | 239 | } |
| 171 | }}> | 240 | }} |
| 172 | <option value={0} key={0}>All Scores</option> | 241 | > |
| 242 | <option value={0} key={0}> | ||
| 243 | All Scores | ||
| 244 | </option> | ||
| 173 | {gameData.map((e, i) => ( | 245 | {gameData.map((e, i) => ( |
| 174 | <option value={e.id} key={i + 1}>{e.name}</option> | 246 | <option value={e.id} key={i + 1}> |
| 175 | ))}</select> | 247 | {e.name} |
| 176 | } | 248 | </option> |
| 249 | ))} | ||
| 250 | </select> | ||
| 251 | )} | ||
| 177 | 252 | ||
| 178 | {game === "0" ? | 253 | {game === '0' ? ( |
| 179 | <select disabled> | 254 | <select disabled> |
| 180 | <option>All Chapters</option> | 255 | <option>All Chapters</option> |
| 181 | </select> | 256 | </select> |
| 182 | : chapterData === null ? <select></select> : | 257 | ) : chapterData === null ? ( |
| 183 | 258 | <select></select> | |
| 184 | <select id='select-chapter' | 259 | ) : ( |
| 185 | onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> | 260 | <select |
| 186 | <option value="0" key="0">All Chapters</option> | 261 | id="select-chapter" |
| 187 | {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( | 262 | onChange={() => |
| 188 | <option value={e.id} key={i + 1}>{e.name}</option> | 263 | setChapter( |
| 189 | ))}</select> | 264 | ( |
| 190 | } | 265 | document.querySelector( |
| 266 | '#select-chapter' | ||
| 267 | ) as HTMLInputElement | ||
| 268 | ).value | ||
| 269 | ) | ||
| 270 | } | ||
| 271 | > | ||
| 272 | <option value="0" key="0"> | ||
| 273 | All Chapters | ||
| 274 | </option> | ||
| 275 | {chapterData.chapters | ||
| 276 | .filter(e => e.is_disabled === false) | ||
| 277 | .map((e, i) => ( | ||
| 278 | <option value={e.id} key={i + 1}> | ||
| 279 | {e.name} | ||
| 280 | </option> | ||
| 281 | ))} | ||
| 282 | </select> | ||
| 283 | )} | ||
| 191 | </div> | 284 | </div> |
| 192 | <div id='profileboard-top'> | 285 | <div id="profileboard-top"> |
| 193 | <span><span>Map Name</span><img src={SortIcon} alt="" /></span> | 286 | <span> |
| 194 | <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> | 287 | <span>Map Name</span> |
| 195 | <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> | 288 | <img src={SortIcon} alt="" /> |
| 196 | <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> | 289 | </span> |
| 290 | <span style={{ justifyContent: 'center' }}> | ||
| 291 | <span>Portals</span> | ||
| 292 | <img src={SortIcon} alt="" /> | ||
| 293 | </span> | ||
| 294 | <span style={{ justifyContent: 'center' }}> | ||
| 295 | <span>WRΔ </span> | ||
| 296 | <img src={SortIcon} alt="" /> | ||
| 297 | </span> | ||
| 298 | <span style={{ justifyContent: 'center' }}> | ||
| 299 | <span>Time</span> | ||
| 300 | <img src={SortIcon} alt="" /> | ||
| 301 | </span> | ||
| 197 | <span> </span> | 302 | <span> </span> |
| 198 | <span><span>Rank</span><img src={SortIcon} alt="" /></span> | 303 | <span> |
| 199 | <span><span>Date</span><img src={SortIcon} alt="" /></span> | 304 | <span>Rank</span> |
| 200 | <div id='page-number'> | 305 | <img src={SortIcon} alt="" /> |
| 306 | </span> | ||
| 307 | <span> | ||
| 308 | <span>Date</span> | ||
| 309 | <img src={SortIcon} alt="" /> | ||
| 310 | </span> | ||
| 311 | <div id="page-number"> | ||
| 201 | <div> | 312 | <div> |
| 202 | <button onClick={() => { | 313 | <button |
| 203 | if (pageNumber !== 1) { | 314 | onClick={() => { |
| 204 | setPageNumber(prevPageNumber => prevPageNumber - 1); | 315 | if (pageNumber !== 1) { |
| 205 | const records = document.querySelectorAll(".profileboard-record"); | 316 | setPageNumber(prevPageNumber => prevPageNumber - 1); |
| 206 | records.forEach((r) => { | 317 | const records = document.querySelectorAll( |
| 207 | (r as HTMLInputElement).style.height = "44px"; | 318 | '.profileboard-record' |
| 208 | }); | 319 | ); |
| 209 | } | 320 | records.forEach(r => { |
| 210 | }} | 321 | (r as HTMLInputElement).style.height = '44px'; |
| 211 | ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> | 322 | }); |
| 212 | <span>{pageNumber}/{pageMax}</span> | 323 | } |
| 213 | <button onClick={() => { | 324 | }} |
| 214 | if (pageNumber !== pageMax) { | 325 | > |
| 215 | setPageNumber(prevPageNumber => prevPageNumber + 1); | 326 | <i |
| 216 | const records = document.querySelectorAll(".profileboard-record"); | 327 | className="triangle" |
| 217 | records.forEach((r) => { | 328 | style={{ position: 'relative', left: '-5px' }} |
| 218 | (r as HTMLInputElement).style.height = "44px"; | 329 | ></i>{' '} |
| 219 | }); | 330 | </button> |
| 220 | } | 331 | <span> |
| 221 | }} | 332 | {pageNumber}/{pageMax} |
| 222 | ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> | 333 | </span> |
| 334 | <button | ||
| 335 | onClick={() => { | ||
| 336 | if (pageNumber !== pageMax) { | ||
| 337 | setPageNumber(prevPageNumber => prevPageNumber + 1); | ||
| 338 | const records = document.querySelectorAll( | ||
| 339 | '.profileboard-record' | ||
| 340 | ); | ||
| 341 | records.forEach(r => { | ||
| 342 | (r as HTMLInputElement).style.height = '44px'; | ||
| 343 | }); | ||
| 344 | } | ||
| 345 | }} | ||
| 346 | > | ||
| 347 | <i | ||
| 348 | className="triangle" | ||
| 349 | style={{ | ||
| 350 | position: 'relative', | ||
| 351 | left: '5px', | ||
| 352 | transform: 'rotate(180deg)', | ||
| 353 | }} | ||
| 354 | ></i>{' '} | ||
| 355 | </button> | ||
| 223 | </div> | 356 | </div> |
| 224 | </div> | 357 | </div> |
| 225 | </div> | 358 | </div> |
| 226 | <hr /> | 359 | <hr /> |
| 227 | <div id='profileboard-records'> | 360 | <div id="profileboard-records"> |
| 228 | 361 | {game === '0' ? ( | |
| 229 | {game === "0" | 362 | user.records |
| 230 | ? ( | 363 | .sort((a, b) => a.map_id - b.map_id) |
| 231 | 364 | .map((r, index) => | |
| 232 | user.records.sort((a, b) => a.map_id - b.map_id) | 365 | Math.ceil((index + 1) / 20) === pageNumber ? ( |
| 233 | .map((r, index) => ( | 366 | <button className="profileboard-record" key={index}> |
| 234 | 367 | {r.scores.map((e, i) => ( | |
| 235 | Math.ceil((index + 1) / 20) === pageNumber ? ( | 368 | <> |
| 236 | <button className="profileboard-record" key={index}> | 369 | {i !== 0 ? ( |
| 237 | {r.scores.map((e, i) => (<> | 370 | <hr style={{ gridColumn: '1 / span 8' }} /> |
| 238 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | 371 | ) : ( |
| 239 | 372 | '' | |
| 240 | <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> | 373 | )} |
| 241 | 374 | ||
| 242 | <span style={{ display: "grid" }}>{e.score_count}</span> | 375 | <Link to={`/maps/${r.map_id}`}> |
| 243 | 376 | <span>{r.map_name}</span> | |
| 244 | <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> | 377 | </Link> |
| 245 | <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> | 378 | |
| 379 | <span style={{ display: 'grid' }}>{e.score_count}</span> | ||
| 380 | |||
| 381 | <span style={{ display: 'grid' }}> | ||
| 382 | {e.score_count - r.map_wr_count > 0 | ||
| 383 | ? `+${e.score_count - r.map_wr_count}` | ||
| 384 | : `-`} | ||
| 385 | </span> | ||
| 386 | <span style={{ display: 'grid' }}> | ||
| 387 | {ticks_to_time(e.score_time)} | ||
| 388 | </span> | ||
| 246 | <span> </span> | 389 | <span> </span> |
| 247 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} | 390 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} |
| 248 | <span>{e.date.split("T")[0]}</span> | 391 | <span>{e.date.split('T')[0]}</span> |
| 249 | <span style={{ flexDirection: "row-reverse" }}> | 392 | <span style={{ flexDirection: 'row-reverse' }}> |
| 250 | 393 | <button | |
| 251 | <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | 394 | onClick={() => { |
| 252 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | 395 | message( |
| 253 | {i === 0 && r.scores.length > 1 ? <button onClick={() => { | 396 | 'Demo Information', |
| 254 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | 397 | `Demo ID: ${e.demo_id}` |
| 255 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | 398 | ); |
| 256 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : | 399 | }} |
| 257 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | 400 | > |
| 258 | } | 401 | <img src={ThreedotIcon} alt="demo_id" /> |
| 259 | }><img src={HistoryIcon} alt="history" /></button> : ""} | 402 | </button> |
| 260 | 403 | <button | |
| 404 | onClick={() => | ||
| 405 | (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`) | ||
| 406 | } | ||
| 407 | > | ||
| 408 | <img src={DownloadIcon} alt="download" /> | ||
| 409 | </button> | ||
| 410 | {i === 0 && r.scores.length > 1 ? ( | ||
| 411 | <button | ||
| 412 | onClick={() => { | ||
| 413 | ( | ||
| 414 | document.querySelectorAll( | ||
| 415 | '.profileboard-record' | ||
| 416 | )[index % 20] as HTMLInputElement | ||
| 417 | ).style.height === '44px' || | ||
| 418 | ( | ||
| 419 | document.querySelectorAll( | ||
| 420 | '.profileboard-record' | ||
| 421 | )[index % 20] as HTMLInputElement | ||
| 422 | ).style.height === '' | ||
| 423 | ? (( | ||
| 424 | document.querySelectorAll( | ||
| 425 | '.profileboard-record' | ||
| 426 | )[index % 20] as HTMLInputElement | ||
| 427 | ).style.height = | ||
| 428 | `${r.scores.length * 46}px`) | ||
| 429 | : (( | ||
| 430 | document.querySelectorAll( | ||
| 431 | '.profileboard-record' | ||
| 432 | )[index % 20] as HTMLInputElement | ||
| 433 | ).style.height = '44px'); | ||
| 434 | }} | ||
| 435 | > | ||
| 436 | <img src={HistoryIcon} alt="history" /> | ||
| 437 | </button> | ||
| 438 | ) : ( | ||
| 439 | '' | ||
| 440 | )} | ||
| 261 | </span> | 441 | </span> |
| 262 | </>))} | 442 | </> |
| 263 | 443 | ))} | |
| 444 | </button> | ||
| 445 | ) : ( | ||
| 446 | '' | ||
| 447 | ) | ||
| 448 | ) | ||
| 449 | ) : maps ? ( | ||
| 450 | maps | ||
| 451 | .filter(e => e.is_disabled === false) | ||
| 452 | .sort((a, b) => a.id - b.id) | ||
| 453 | .map((r, index) => { | ||
| 454 | if (Math.ceil((index + 1) / 20) === pageNumber) { | ||
| 455 | let record = user.records.find(e => e.map_id === r.id); | ||
| 456 | return record === undefined ? ( | ||
| 457 | <button | ||
| 458 | className="profileboard-record" | ||
| 459 | key={index} | ||
| 460 | style={{ backgroundColor: '#1b1b20' }} | ||
| 461 | > | ||
| 462 | <Link to={`/maps/${r.id}`}> | ||
| 463 | <span>{r.name}</span> | ||
| 464 | </Link> | ||
| 465 | <span style={{ display: 'grid' }}>N/A</span> | ||
| 466 | <span style={{ display: 'grid' }}>N/A</span> | ||
| 467 | <span>N/A</span> | ||
| 468 | <span> </span> | ||
| 469 | <span>N/A</span> | ||
| 470 | <span>N/A</span> | ||
| 471 | <span style={{ flexDirection: 'row-reverse' }}></span> | ||
| 264 | </button> | 472 | </button> |
| 265 | ) : "" | 473 | ) : ( |
| 266 | ))) : maps ? | 474 | <button className="profileboard-record" key={index}> |
| 267 | 475 | {record.scores.map((e, i) => ( | |
| 268 | maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) | 476 | <> |
| 269 | .map((r, index) => { | 477 | {i !== 0 ? ( |
| 270 | if (Math.ceil((index + 1) / 20) === pageNumber) { | 478 | <hr style={{ gridColumn: '1 / span 8' }} /> |
| 271 | let record = user.records.find((e) => e.map_id === r.id); | 479 | ) : ( |
| 272 | return record === undefined ? ( | 480 | '' |
| 273 | <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> | 481 | )} |
| 274 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 482 | <Link to={`/maps/${r.id}`}> |
| 275 | <span style={{ display: "grid" }}>N/A</span> | 483 | <span>{r.name}</span> |
| 276 | <span style={{ display: "grid" }}>N/A</span> | 484 | </Link> |
| 277 | <span>N/A</span> | 485 | <span style={{ display: 'grid' }}> |
| 278 | <span> </span> | 486 | {record!.scores[i].score_count} |
| 279 | <span>N/A</span> | 487 | </span> |
| 280 | <span>N/A</span> | 488 | <span style={{ display: 'grid' }}> |
| 281 | <span style={{ flexDirection: "row-reverse" }}></span> | 489 | {record!.scores[i].score_count - |
| 282 | </button> | 490 | record!.map_wr_count > |
| 283 | ) : ( | 491 | 0 |
| 284 | <button className="profileboard-record" key={index}> | 492 | ? `+${record!.scores[i].score_count - record!.map_wr_count}` |
| 285 | {record.scores.map((e, i) => (<> | 493 | : `-`} |
| 286 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | 494 | </span> |
| 287 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 495 | <span style={{ display: 'grid' }}> |
| 288 | <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> | 496 | {ticks_to_time(record!.scores[i].score_time)} |
| 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> | 497 | </span> |
| 290 | <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> | ||
| 291 | <span> </span> | 498 | <span> </span> |
| 292 | {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} | 499 | {i === 0 ? ( |
| 293 | <span>{record!.scores[i].date.split("T")[0]}</span> | 500 | <span>#{record!.placement}</span> |
| 294 | <span style={{ flexDirection: "row-reverse" }}> | 501 | ) : ( |
| 295 | 502 | <span> </span> | |
| 296 | <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | 503 | )} |
| 297 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | 504 | <span>{record!.scores[i].date.split('T')[0]}</span> |
| 298 | {i === 0 && record!.scores.length > 1 ? <button onClick={() => { | 505 | <span style={{ flexDirection: 'row-reverse' }}> |
| 299 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | 506 | <button |
| 300 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | 507 | onClick={() => { |
| 301 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : | 508 | message( |
| 302 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | 509 | 'Demo Information', |
| 303 | } | 510 | `Demo ID: ${e.demo_id}` |
| 304 | }><img src={HistoryIcon} alt="history" /></button> : ""} | 511 | ); |
| 305 | 512 | }} | |
| 513 | > | ||
| 514 | <img src={ThreedotIcon} alt="demo_id" /> | ||
| 515 | </button> | ||
| 516 | <button | ||
| 517 | onClick={() => | ||
| 518 | (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`) | ||
| 519 | } | ||
| 520 | > | ||
| 521 | <img src={DownloadIcon} alt="download" /> | ||
| 522 | </button> | ||
| 523 | {i === 0 && record!.scores.length > 1 ? ( | ||
| 524 | <button | ||
| 525 | onClick={() => { | ||
| 526 | ( | ||
| 527 | document.querySelectorAll( | ||
| 528 | '.profileboard-record' | ||
| 529 | )[index % 20] as HTMLInputElement | ||
| 530 | ).style.height === '44px' || | ||
| 531 | ( | ||
| 532 | document.querySelectorAll( | ||
| 533 | '.profileboard-record' | ||
| 534 | )[index % 20] as HTMLInputElement | ||
| 535 | ).style.height === '' | ||
| 536 | ? (( | ||
| 537 | document.querySelectorAll( | ||
| 538 | '.profileboard-record' | ||
| 539 | )[index % 20] as HTMLInputElement | ||
| 540 | ).style.height = | ||
| 541 | `${record!.scores.length * 46}px`) | ||
| 542 | : (( | ||
| 543 | document.querySelectorAll( | ||
| 544 | '.profileboard-record' | ||
| 545 | )[index % 20] as HTMLInputElement | ||
| 546 | ).style.height = '44px'); | ||
| 547 | }} | ||
| 548 | > | ||
| 549 | <img src={HistoryIcon} alt="history" /> | ||
| 550 | </button> | ||
| 551 | ) : ( | ||
| 552 | '' | ||
| 553 | )} | ||
| 306 | </span> | 554 | </span> |
| 307 | </>))} | 555 | </> |
| 308 | </button> | 556 | ))} |
| 309 | 557 | </button> | |
| 310 | ) | 558 | ); |
| 311 | } else { return null } | 559 | } else { |
| 312 | }) : (<>{console.warn(maps)}</>)} | 560 | return null; |
| 561 | } | ||
| 562 | }) | ||
| 563 | ) : ( | ||
| 564 | <>{console.warn(maps)}</> | ||
| 565 | )} | ||
| 313 | </div> | 566 | </div> |
| 314 | </section> | 567 | </section> |
| 315 | </main> | 568 | </main> |
diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts index 8265915..9bf083a 100644 --- a/frontend/src/react-app-env.d.ts +++ b/frontend/src/react-app-env.d.ts | |||
| @@ -1,2 +1,2 @@ | |||
| 1 | declare module "*.png"; | 1 | declare module '*.png'; |
| 2 | declare module "*.css"; | 2 | declare module '*.css'; |
diff --git a/frontend/src/types/Chapters.ts b/frontend/src/types/Chapters.ts index 1d48306..8924b97 100644 --- a/frontend/src/types/Chapters.ts +++ b/frontend/src/types/Chapters.ts | |||
| @@ -1,19 +1,19 @@ | |||
| 1 | import type { Game } from "@customTypes/Game"; | 1 | import type { Game } from '@customTypes/Game'; |
| 2 | import type { Map } from "@customTypes/Map"; | 2 | import type { Map } from '@customTypes/Map'; |
| 3 | 3 | ||
| 4 | interface Chapter { | 4 | interface Chapter { |
| 5 | id: number; | 5 | id: number; |
| 6 | name: string; | 6 | name: string; |
| 7 | image: string; | 7 | image: string; |
| 8 | is_disabled: boolean; | 8 | is_disabled: boolean; |
| 9 | } | 9 | } |
| 10 | 10 | ||
| 11 | export interface GameChapter { | 11 | export interface GameChapter { |
| 12 | chapter: Chapter; | 12 | chapter: Chapter; |
| 13 | maps: Map[]; | 13 | maps: Map[]; |
| 14 | } | 14 | } |
| 15 | 15 | ||
| 16 | export interface GamesChapters { | 16 | export interface GamesChapters { |
| 17 | game: Game; | 17 | game: Game; |
| 18 | chapters: Chapter[]; | 18 | chapters: Chapter[]; |
| 19 | } \ No newline at end of file | 19 | } |
diff --git a/frontend/src/types/Content.ts b/frontend/src/types/Content.ts index 775fab4..77b3970 100644 --- a/frontend/src/types/Content.ts +++ b/frontend/src/types/Content.ts | |||
| @@ -6,18 +6,18 @@ export interface ModMenuContent { | |||
| 6 | showcase: string; | 6 | showcase: string; |
| 7 | description: string; | 7 | description: string; |
| 8 | category_id: number; | 8 | category_id: number; |
| 9 | }; | 9 | } |
| 10 | 10 | ||
| 11 | export interface MapDiscussionContent { | 11 | export interface MapDiscussionContent { |
| 12 | title: string; | 12 | title: string; |
| 13 | content: string; | 13 | content: string; |
| 14 | }; | 14 | } |
| 15 | 15 | ||
| 16 | export interface MapDiscussionCommentContent { | 16 | export interface MapDiscussionCommentContent { |
| 17 | comment: string; | 17 | comment: string; |
| 18 | }; | 18 | } |
| 19 | 19 | ||
| 20 | export interface UploadRunContent { | 20 | export interface UploadRunContent { |
| 21 | host_demo: File | null; | 21 | host_demo: File | null; |
| 22 | partner_demo: File | null; | 22 | partner_demo: File | null; |
| 23 | }; | 23 | } |
diff --git a/frontend/src/types/Game.ts b/frontend/src/types/Game.ts index 1a80341..be2cd73 100644 --- a/frontend/src/types/Game.ts +++ b/frontend/src/types/Game.ts | |||
| @@ -1,37 +1,36 @@ | |||
| 1 | import type { Map } from '@customTypes/Map'; | 1 | import type { Map } from '@customTypes/Map'; |
| 2 | 2 | ||
| 3 | |||
| 4 | export interface Game { | 3 | export interface Game { |
| 5 | id: number; | 4 | id: number; |
| 6 | name: string; | 5 | name: string; |
| 7 | image: string; | 6 | image: string; |
| 8 | is_coop: boolean; | 7 | is_coop: boolean; |
| 9 | category_portals: GameCategoryPortals[]; | 8 | category_portals: GameCategoryPortals[]; |
| 10 | }; | 9 | } |
| 11 | 10 | ||
| 12 | export interface GameChapters { | 11 | export interface GameChapters { |
| 13 | game: Game; | 12 | game: Game; |
| 14 | chapters: Chapter[]; | 13 | chapters: Chapter[]; |
| 15 | }; | 14 | } |
| 16 | 15 | ||
| 17 | export interface GameMaps { | 16 | export interface GameMaps { |
| 18 | game: Game; | 17 | game: Game; |
| 19 | maps: Map[]; | 18 | maps: Map[]; |
| 20 | }; | 19 | } |
| 21 | 20 | ||
| 22 | export interface Category { | 21 | export interface Category { |
| 23 | id: number; | 22 | id: number; |
| 24 | name: string; | 23 | name: string; |
| 25 | }; | 24 | } |
| 26 | 25 | ||
| 27 | interface Chapter { | 26 | interface Chapter { |
| 28 | id: number; | 27 | id: number; |
| 29 | name: string; | 28 | name: string; |
| 30 | image: string; | 29 | image: string; |
| 31 | is_disabled: boolean; | 30 | is_disabled: boolean; |
| 32 | }; | 31 | } |
| 33 | 32 | ||
| 34 | export interface GameCategoryPortals { | 33 | export interface GameCategoryPortals { |
| 35 | category: Category; | 34 | category: Category; |
| 36 | portal_count: number; | 35 | portal_count: number; |
| 37 | }; | 36 | } |
diff --git a/frontend/src/types/Map.ts b/frontend/src/types/Map.ts index 89c66d5..6bc6369 100644 --- a/frontend/src/types/Map.ts +++ b/frontend/src/types/Map.ts | |||
| @@ -9,15 +9,15 @@ export interface Map { | |||
| 9 | is_disabled: boolean; | 9 | is_disabled: boolean; |
| 10 | difficulty: number; | 10 | difficulty: number; |
| 11 | category_portals: GameCategoryPortals[]; | 11 | category_portals: GameCategoryPortals[]; |
| 12 | }; | 12 | } |
| 13 | 13 | ||
| 14 | export interface MapDiscussion { | 14 | export interface MapDiscussion { |
| 15 | discussion: MapDiscussionsDetail; | 15 | discussion: MapDiscussionsDetail; |
| 16 | }; | 16 | } |
| 17 | 17 | ||
| 18 | export interface MapDiscussions { | 18 | export interface MapDiscussions { |
| 19 | discussions: MapDiscussionsDetail[]; | 19 | discussions: MapDiscussionsDetail[]; |
| 20 | }; | 20 | } |
| 21 | 21 | ||
| 22 | export interface MapDiscussionsDetail { | 22 | export interface MapDiscussionsDetail { |
| 23 | id: number; | 23 | id: number; |
| @@ -27,22 +27,24 @@ export interface MapDiscussionsDetail { | |||
| 27 | comments: MapDiscussionDetailComment[]; | 27 | comments: MapDiscussionDetailComment[]; |
| 28 | created_at: string; | 28 | created_at: string; |
| 29 | updated_at: string; | 29 | updated_at: string; |
| 30 | }; | 30 | } |
| 31 | 31 | ||
| 32 | interface MapDiscussionDetailComment { | 32 | interface MapDiscussionDetailComment { |
| 33 | comment: string; | 33 | comment: string; |
| 34 | date: string; | 34 | date: string; |
| 35 | user: UserShort; | 35 | user: UserShort; |
| 36 | }; | 36 | } |
| 37 | 37 | ||
| 38 | export interface MapLeaderboard { | 38 | export interface MapLeaderboard { |
| 39 | map: MapSummaryMap; | 39 | map: MapSummaryMap; |
| 40 | records: MapLeaderboardRecordSingleplayer[] | MapLeaderboardRecordMultiplayer[]; | 40 | records: |
| 41 | | MapLeaderboardRecordSingleplayer[] | ||
| 42 | | MapLeaderboardRecordMultiplayer[]; | ||
| 41 | pagination: Pagination; | 43 | pagination: Pagination; |
| 42 | }; | 44 | } |
| 43 | 45 | ||
| 44 | export interface MapLeaderboardRecordSingleplayer { | 46 | export interface MapLeaderboardRecordSingleplayer { |
| 45 | kind: "singleplayer"; | 47 | kind: 'singleplayer'; |
| 46 | placement: number; | 48 | placement: number; |
| 47 | record_id: number; | 49 | record_id: number; |
| 48 | score_count: number; | 50 | score_count: number; |
| @@ -50,10 +52,10 @@ export interface MapLeaderboardRecordSingleplayer { | |||
| 50 | user: UserShort; | 52 | user: UserShort; |
| 51 | demo_id: string; | 53 | demo_id: string; |
| 52 | record_date: string; | 54 | record_date: string; |
| 53 | }; | 55 | } |
| 54 | 56 | ||
| 55 | export interface MapLeaderboardRecordMultiplayer { | 57 | export interface MapLeaderboardRecordMultiplayer { |
| 56 | kind: "multiplayer"; | 58 | kind: 'multiplayer'; |
| 57 | placement: number; | 59 | placement: number; |
| 58 | record_id: number; | 60 | record_id: number; |
| 59 | score_count: number; | 61 | score_count: number; |
| @@ -63,13 +65,12 @@ export interface MapLeaderboardRecordMultiplayer { | |||
| 63 | host_demo_id: string; | 65 | host_demo_id: string; |
| 64 | partner_demo_id: string; | 66 | partner_demo_id: string; |
| 65 | record_date: string; | 67 | record_date: string; |
| 66 | }; | 68 | } |
| 67 | |||
| 68 | 69 | ||
| 69 | export interface MapSummary { | 70 | export interface MapSummary { |
| 70 | map: MapSummaryMap; | 71 | map: MapSummaryMap; |
| 71 | summary: MapSummaryDetails; | 72 | summary: MapSummaryDetails; |
| 72 | }; | 73 | } |
| 73 | 74 | ||
| 74 | interface MapSummaryMap { | 75 | interface MapSummaryMap { |
| 75 | id: number; | 76 | id: number; |
| @@ -79,11 +80,11 @@ interface MapSummaryMap { | |||
| 79 | map_name: string; | 80 | map_name: string; |
| 80 | is_coop: boolean; | 81 | is_coop: boolean; |
| 81 | is_disabled: boolean; | 82 | is_disabled: boolean; |
| 82 | }; | 83 | } |
| 83 | 84 | ||
| 84 | interface MapSummaryDetails { | 85 | interface MapSummaryDetails { |
| 85 | routes: MapSummaryDetailsRoute[]; | 86 | routes: MapSummaryDetailsRoute[]; |
| 86 | }; | 87 | } |
| 87 | 88 | ||
| 88 | interface MapSummaryDetailsRoute { | 89 | interface MapSummaryDetailsRoute { |
| 89 | route_id: number; | 90 | route_id: number; |
| @@ -93,16 +94,15 @@ interface MapSummaryDetailsRoute { | |||
| 93 | completion_count: number; | 94 | completion_count: number; |
| 94 | description: string; | 95 | description: string; |
| 95 | showcase: string; | 96 | showcase: string; |
| 96 | }; | 97 | } |
| 97 | 98 | ||
| 98 | interface MapSummaryDetailsRouteHistory { | 99 | interface MapSummaryDetailsRouteHistory { |
| 99 | runner_name: string; | 100 | runner_name: string; |
| 100 | score_count: number; | 101 | score_count: number; |
| 101 | date: string; | 102 | date: string; |
| 102 | }; | 103 | } |
| 103 | 104 | ||
| 104 | export interface MapDeleteEndpoint { | 105 | export interface MapDeleteEndpoint { |
| 105 | map_id: number; | 106 | map_id: number; |
| 106 | record_id: number; | 107 | record_id: number; |
| 107 | } | 108 | } |
| 108 | |||
diff --git a/frontend/src/types/MapNames.ts b/frontend/src/types/MapNames.ts index b6313e7..9ea9851 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/types/Pagination.ts b/frontend/src/types/Pagination.ts index ccff04b..18494eb 100644 --- a/frontend/src/types/Pagination.ts +++ b/frontend/src/types/Pagination.ts | |||
| @@ -3,4 +3,4 @@ export interface Pagination { | |||
| 3 | total_pages: number; | 3 | total_pages: number; |
| 4 | current_page: number; | 4 | current_page: number; |
| 5 | page_size: number; | 5 | page_size: number; |
| 6 | }; | 6 | } |
diff --git a/frontend/src/types/Profile.ts b/frontend/src/types/Profile.ts index 42e5c3e..8051ae5 100644 --- a/frontend/src/types/Profile.ts +++ b/frontend/src/types/Profile.ts | |||
| @@ -1,10 +1,10 @@ | |||
| 1 | import type { Pagination } from "@customTypes/Pagination"; | 1 | import type { Pagination } from '@customTypes/Pagination'; |
| 2 | 2 | ||
| 3 | export interface UserShort { | 3 | export interface UserShort { |
| 4 | steam_id: string; | 4 | steam_id: string; |
| 5 | user_name: string; | 5 | user_name: string; |
| 6 | avatar_link: string; | 6 | avatar_link: string; |
| 7 | }; | 7 | } |
| 8 | 8 | ||
| 9 | export interface UserProfile { | 9 | export interface UserProfile { |
| 10 | profile: boolean; | 10 | profile: boolean; |
| @@ -17,25 +17,25 @@ export interface UserProfile { | |||
| 17 | rankings: UserProfileRankings; | 17 | rankings: UserProfileRankings; |
| 18 | records: UserProfileRecords[]; | 18 | records: UserProfileRecords[]; |
| 19 | pagination: Pagination; | 19 | pagination: Pagination; |
| 20 | }; | 20 | } |
| 21 | 21 | ||
| 22 | interface UserProfileTitles { | 22 | interface UserProfileTitles { |
| 23 | name: string; | 23 | name: string; |
| 24 | color: string; | 24 | color: string; |
| 25 | }; | 25 | } |
| 26 | 26 | ||
| 27 | interface UserProfileLinks { | 27 | interface UserProfileLinks { |
| 28 | p2sr: string; | 28 | p2sr: string; |
| 29 | steam: string; | 29 | steam: string; |
| 30 | youtube: string; | 30 | youtube: string; |
| 31 | twitch: string; | 31 | twitch: string; |
| 32 | }; | 32 | } |
| 33 | 33 | ||
| 34 | interface UserProfileRankings { | 34 | interface UserProfileRankings { |
| 35 | overall: UserProfileRankingsDetail; | 35 | overall: UserProfileRankingsDetail; |
| 36 | singleplayer: UserProfileRankingsDetail; | 36 | singleplayer: UserProfileRankingsDetail; |
| 37 | cooperative: UserProfileRankingsDetail; | 37 | cooperative: UserProfileRankingsDetail; |
| 38 | }; | 38 | } |
| 39 | 39 | ||
| 40 | interface UserProfileRecords { | 40 | interface UserProfileRecords { |
| 41 | game_id: number; | 41 | game_id: number; |
| @@ -44,8 +44,8 @@ interface UserProfileRecords { | |||
| 44 | map_name: string; | 44 | map_name: string; |
| 45 | map_wr_count: number; | 45 | map_wr_count: number; |
| 46 | placement: number; | 46 | placement: number; |
| 47 | scores: UserProfileRecordsScores[] | 47 | scores: UserProfileRecordsScores[]; |
| 48 | }; | 48 | } |
| 49 | 49 | ||
| 50 | interface UserProfileRecordsScores { | 50 | interface UserProfileRecordsScores { |
| 51 | record_id: number; | 51 | record_id: number; |
| @@ -53,11 +53,10 @@ interface UserProfileRecordsScores { | |||
| 53 | score_count: number; | 53 | score_count: number; |
| 54 | score_time: number; | 54 | score_time: number; |
| 55 | date: string; | 55 | date: string; |
| 56 | }; | 56 | } |
| 57 | 57 | ||
| 58 | interface UserProfileRankingsDetail { | 58 | interface UserProfileRankingsDetail { |
| 59 | rank: number; | 59 | rank: number; |
| 60 | completion_count: number; | 60 | completion_count: number; |
| 61 | completion_total: number; | 61 | completion_total: number; |
| 62 | }; | 62 | } |
| 63 | |||
diff --git a/frontend/src/types/Ranking.ts b/frontend/src/types/Ranking.ts index a143355..06a5ca4 100644 --- a/frontend/src/types/Ranking.ts +++ b/frontend/src/types/Ranking.ts | |||
| @@ -1,31 +1,31 @@ | |||
| 1 | import type { UserShort } from "@customTypes/Profile"; | 1 | import type { UserShort } from '@customTypes/Profile'; |
| 2 | 2 | ||
| 3 | export interface RankingType { | 3 | export interface RankingType { |
| 4 | placement: number; | 4 | placement: number; |
| 5 | user: UserShort; | 5 | user: UserShort; |
| 6 | total_score: number; | 6 | total_score: number; |
| 7 | } | 7 | } |
| 8 | 8 | ||
| 9 | export interface SteamRankingType { | 9 | export interface SteamRankingType { |
| 10 | user_name: string; | 10 | user_name: string; |
| 11 | avatar_link: string; | 11 | avatar_link: string; |
| 12 | steam_id: string; | 12 | steam_id: string; |
| 13 | sp_score: number; | 13 | sp_score: number; |
| 14 | mp_score: number; | 14 | mp_score: number; |
| 15 | overall_score: number; | 15 | overall_score: number; |
| 16 | sp_rank: number; | 16 | sp_rank: number; |
| 17 | mp_rank: number; | 17 | mp_rank: number; |
| 18 | overall_rank: number; | 18 | overall_rank: number; |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | export interface Ranking { | 21 | export interface Ranking { |
| 22 | rankings_overall: RankingType[]; | 22 | rankings_overall: RankingType[]; |
| 23 | rankings_singleplayer: RankingType[]; | 23 | rankings_singleplayer: RankingType[]; |
| 24 | rankings_multiplayer: RankingType[]; | 24 | rankings_multiplayer: RankingType[]; |
| 25 | } | 25 | } |
| 26 | 26 | ||
| 27 | export interface SteamRanking { | 27 | export interface SteamRanking { |
| 28 | rankings_overall: SteamRankingType[]; | 28 | rankings_overall: SteamRankingType[]; |
| 29 | rankings_singleplayer: SteamRankingType[]; | 29 | rankings_singleplayer: SteamRankingType[]; |
| 30 | rankings_multiplayer: SteamRankingType[]; | 30 | rankings_multiplayer: SteamRankingType[]; |
| 31 | } \ No newline at end of file | 31 | } |
diff --git a/frontend/src/types/Search.ts b/frontend/src/types/Search.ts index d218806..48b9169 100644 --- a/frontend/src/types/Search.ts +++ b/frontend/src/types/Search.ts | |||
| @@ -1,13 +1,13 @@ | |||
| 1 | import type { UserShort } from "@customTypes/Profile"; | 1 | import type { UserShort } from '@customTypes/Profile'; |
| 2 | 2 | ||
| 3 | export interface Search { | 3 | export interface Search { |
| 4 | players: UserShort[]; | 4 | players: UserShort[]; |
| 5 | maps: SearchMap[]; | 5 | maps: SearchMap[]; |
| 6 | }; | 6 | } |
| 7 | 7 | ||
| 8 | interface SearchMap { | 8 | interface SearchMap { |
| 9 | id: number; | 9 | id: number; |
| 10 | game: string; | 10 | game: string; |
| 11 | chapter: string; | 11 | chapter: string; |
| 12 | map: string; | 12 | map: string; |
| 13 | }; | 13 | } |
diff --git a/frontend/src/utils/Jwt.ts b/frontend/src/utils/Jwt.ts index ce351fb..affcd36 100644 --- a/frontend/src/utils/Jwt.ts +++ b/frontend/src/utils/Jwt.ts | |||
| @@ -1,5 +1,7 @@ | |||
| 1 | // llm ahh funcs | 1 | // llm ahh funcs |
| 2 | export function get_user_id_from_token(token: string | undefined): string | undefined { | 2 | export function get_user_id_from_token( |
| 3 | token: string | undefined | ||
| 4 | ): string | undefined { | ||
| 3 | if (!token) { | 5 | if (!token) { |
| 4 | return undefined; | 6 | return undefined; |
| 5 | } | 7 | } |
| @@ -19,9 +21,11 @@ export function get_user_id_from_token(token: string | undefined): string | unde | |||
| 19 | .join('') | 21 | .join('') |
| 20 | ); | 22 | ); |
| 21 | return JSON.parse(jsonPayload).sub; | 23 | return JSON.parse(jsonPayload).sub; |
| 22 | }; | 24 | } |
| 23 | 25 | ||
| 24 | export function get_user_mod_from_token(token: string | undefined): boolean | undefined { | 26 | export function get_user_mod_from_token( |
| 27 | token: string | undefined | ||
| 28 | ): boolean | undefined { | ||
| 25 | if (!token) { | 29 | if (!token) { |
| 26 | return undefined; | 30 | return undefined; |
| 27 | } | 31 | } |
| @@ -41,4 +45,4 @@ export function get_user_mod_from_token(token: string | undefined): boolean | un | |||
| 41 | .join('') | 45 | .join('') |
| 42 | ); | 46 | ); |
| 43 | return JSON.parse(jsonPayload).mod; | 47 | return JSON.parse(jsonPayload).mod; |
| 44 | }; | 48 | } |
diff --git a/frontend/src/utils/Time.ts b/frontend/src/utils/Time.ts index b83a7ed..34830b0 100644 --- a/frontend/src/utils/Time.ts +++ b/frontend/src/utils/Time.ts | |||
| @@ -1,42 +1,62 @@ | |||
| 1 | export function time_ago(date: any) { | 1 | export function time_ago(date: any) { |
| 2 | const now = new Date().getTime(); | 2 | const now = new Date().getTime(); |
| 3 | 3 | ||
| 4 | const localDate = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); | 4 | const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000); |
| 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) { |
| 9 | if (interval > 1) {return interval + ' years ago';} | 9 | return interval + ' year ago'; |
| 10 | } | ||
| 11 | if (interval > 1) { | ||
| 12 | return interval + ' years ago'; | ||
| 13 | } | ||
| 10 | 14 | ||
| 11 | interval = Math.floor(seconds / 2592000); | 15 | interval = Math.floor(seconds / 2592000); |
| 12 | if (interval === 1) {return interval + ' month ago';} | 16 | if (interval === 1) { |
| 13 | if (interval > 1) {return interval + ' months ago';} | 17 | return interval + ' month ago'; |
| 18 | } | ||
| 19 | if (interval > 1) { | ||
| 20 | return interval + ' months ago'; | ||
| 21 | } | ||
| 14 | 22 | ||
| 15 | interval = Math.floor(seconds / 86400); | 23 | interval = Math.floor(seconds / 86400); |
| 16 | if (interval === 1) {return interval + ' day ago';} | 24 | if (interval === 1) { |
| 17 | if (interval > 1) {return interval + ' days ago';} | 25 | return interval + ' day ago'; |
| 26 | } | ||
| 27 | if (interval > 1) { | ||
| 28 | return interval + ' days ago'; | ||
| 29 | } | ||
| 18 | 30 | ||
| 19 | interval = Math.floor(seconds / 3600); | 31 | interval = Math.floor(seconds / 3600); |
| 20 | if (interval === 1) {return interval + ' hour ago';} | 32 | if (interval === 1) { |
| 21 | if (interval > 1) {return interval + ' hours ago';} | 33 | return interval + ' hour ago'; |
| 34 | } | ||
| 35 | if (interval > 1) { | ||
| 36 | return interval + ' hours ago'; | ||
| 37 | } | ||
| 22 | 38 | ||
| 23 | interval = Math.floor(seconds / 60); | 39 | interval = Math.floor(seconds / 60); |
| 24 | if (interval === 1) {return interval + ' minute ago';} | 40 | if (interval === 1) { |
| 25 | if (interval > 1) {return interval + ' minutes ago';} | 41 | return interval + ' minute ago'; |
| 42 | } | ||
| 43 | if (interval > 1) { | ||
| 44 | return interval + ' minutes ago'; | ||
| 45 | } | ||
| 26 | 46 | ||
| 27 | if(seconds < 10) return 'just now'; | 47 | if (seconds < 10) return 'just now'; |
| 28 | 48 | ||
| 29 | return Math.floor(seconds) + ' seconds ago'; | 49 | return Math.floor(seconds) + ' seconds ago'; |
| 30 | }; | 50 | } |
| 31 | 51 | ||
| 32 | export function ticks_to_time(ticks: number) { | 52 | export function ticks_to_time(ticks: number) { |
| 33 | let seconds = Math.floor(ticks / 60) | 53 | let seconds = Math.floor(ticks / 60); |
| 34 | let minutes = Math.floor(seconds / 60) | 54 | let minutes = Math.floor(seconds / 60); |
| 35 | let hours = Math.floor(minutes / 60) | 55 | let hours = Math.floor(minutes / 60); |
| 36 | 56 | ||
| 37 | let milliseconds = Math.floor((ticks % 60) * 1000 / 60) | 57 | let milliseconds = Math.floor(((ticks % 60) * 1000) / 60); |
| 38 | seconds = seconds % 60; | 58 | seconds = seconds % 60; |
| 39 | minutes = minutes % 60; | 59 | minutes = minutes % 60; |
| 40 | 60 | ||
| 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')}`; | 61 | 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 | 62 | } |