diff options
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/App.css | 17 | ||||
| -rw-r--r-- | frontend/src/App.js | 49 | ||||
| -rw-r--r-- | frontend/src/App.tsx | 40 | ||||
| -rw-r--r-- | frontend/src/api/Api.tsx | 157 | ||||
| -rw-r--r-- | frontend/src/components/Discussions.tsx | 151 | ||||
| -rw-r--r-- | frontend/src/components/GameEntry.tsx | 49 | ||||
| -rw-r--r-- | frontend/src/components/Leaderboards.tsx | 105 | ||||
| -rw-r--r-- | frontend/src/components/Login.tsx | 1931 | ||||
| -rw-r--r-- | frontend/src/components/ModMenu.tsx | 324 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar.tsx | 183 | ||||
| -rw-r--r-- | frontend/src/components/Summary.tsx | 169 | ||||
| -rw-r--r-- | frontend/src/components/login.js | 61 | ||||
| -rw-r--r-- | frontend/src/components/main.css | 17 | ||||
| -rw-r--r-- | frontend/src/components/main.js | 17 | ||||
| -rw-r--r-- | frontend/src/components/news.css | 29 | ||||
| -rw-r--r-- | frontend/src/components/news.js | 21 | ||||
| -rw-r--r-- | frontend/src/components/pages/about.css | 17 | ||||
| -rw-r--r-- | frontend/src/components/pages/about.js | 32 | ||||
| -rw-r--r-- | frontend/src/components/pages/game.js | 46 | ||||
| -rw-r--r-- | frontend/src/components/pages/games.js | 62 | ||||
| -rw-r--r-- | frontend/src/components/pages/home.css | 92 | ||||
| -rw-r--r-- | frontend/src/components/pages/home.js | 242 | ||||
| -rw-r--r-- | frontend/src/components/pages/maplist.css | 403 | ||||
| -rw-r--r-- | frontend/src/components/pages/maplist.js | 890 | ||||
| -rw-r--r-- | frontend/src/components/pages/profile.js | 382 | ||||
| -rw-r--r-- | frontend/src/components/pages/summary.js | 650 | ||||
| -rw-r--r-- | frontend/src/components/pages/summary_modview.js | 254 | ||||
| -rw-r--r-- | frontend/src/components/record.css | 15 | ||||
| -rw-r--r-- | frontend/src/components/record.js | 56 | ||||
| -rw-r--r-- | frontend/src/components/sidebar.js | 203 | ||||
| -rw-r--r-- | frontend/src/css/Games.css (renamed from frontend/src/components/pages/games.css) | 0 | ||||
| -rw-r--r-- | frontend/src/css/Login.css (renamed from frontend/src/components/login.css) | 2 | ||||
| -rw-r--r-- | frontend/src/css/Maps.css (renamed from frontend/src/components/pages/summary.css) | 10 | ||||
| -rw-r--r-- | frontend/src/css/ModMenu.css (renamed from frontend/src/components/pages/summary_modview.css) | 0 | ||||
| -rw-r--r-- | frontend/src/css/Profile.css (renamed from frontend/src/components/pages/profile.css) | 0 | ||||
| -rw-r--r-- | frontend/src/css/Sidebar.css (renamed from frontend/src/components/sidebar.css) | 0 | ||||
| -rw-r--r-- | frontend/src/images/Images.tsx | 44 | ||||
| -rw-r--r-- | frontend/src/images/png/1.png (renamed from frontend/src/imgs/1.png) | bin | 2011 -> 2011 bytes | |||
| -rw-r--r-- | frontend/src/images/png/10.png (renamed from frontend/src/imgs/10.png) | bin | 1601 -> 1601 bytes | |||
| -rw-r--r-- | frontend/src/images/png/11.png (renamed from frontend/src/imgs/11.png) | bin | 1294 -> 1294 bytes | |||
| -rw-r--r-- | frontend/src/images/png/12.png (renamed from frontend/src/imgs/12.png) | bin | 1545 -> 1545 bytes | |||
| -rw-r--r-- | frontend/src/images/png/13.png (renamed from frontend/src/imgs/13.png) | bin | 1251 -> 1251 bytes | |||
| -rw-r--r-- | frontend/src/images/png/14.png (renamed from frontend/src/imgs/14.png) | bin | 1363 -> 1363 bytes | |||
| -rw-r--r-- | frontend/src/images/png/15.png (renamed from frontend/src/imgs/15.png) | bin | 2988 -> 2988 bytes | |||
| -rw-r--r-- | frontend/src/images/png/16.png (renamed from frontend/src/imgs/16.png) | bin | 3078 -> 3078 bytes | |||
| -rw-r--r-- | frontend/src/images/png/17.png (renamed from frontend/src/imgs/17.png) | bin | 4943 -> 4943 bytes | |||
| -rw-r--r-- | frontend/src/images/png/18.png (renamed from frontend/src/imgs/18.png) | bin | 2434 -> 2434 bytes | |||
| -rw-r--r-- | frontend/src/images/png/19.png (renamed from frontend/src/imgs/19.png) | bin | 1266 -> 1266 bytes | |||
| -rw-r--r-- | frontend/src/images/png/2.png (renamed from frontend/src/imgs/2.png) | bin | 1833 -> 1833 bytes | |||
| -rw-r--r-- | frontend/src/images/png/3.png (renamed from frontend/src/imgs/3.png) | bin | 1517 -> 1517 bytes | |||
| -rw-r--r-- | frontend/src/images/png/4.png (renamed from frontend/src/imgs/4.png) | bin | 4517 -> 4517 bytes | |||
| -rw-r--r-- | frontend/src/images/png/5.png (renamed from frontend/src/imgs/5.png) | bin | 4112 -> 4112 bytes | |||
| -rw-r--r-- | frontend/src/images/png/6.png (renamed from frontend/src/imgs/6.png) | bin | 2715 -> 2715 bytes | |||
| -rw-r--r-- | frontend/src/images/png/7.png (renamed from frontend/src/imgs/7.png) | bin | 1608 -> 1608 bytes | |||
| -rw-r--r-- | frontend/src/images/png/8.png (renamed from frontend/src/imgs/8.png) | bin | 1584 -> 1584 bytes | |||
| -rw-r--r-- | frontend/src/images/png/9.png (renamed from frontend/src/imgs/9.png) | bin | 6037 -> 6037 bytes | |||
| -rw-r--r-- | frontend/src/images/png/login.png (renamed from frontend/src/imgs/login.png) | bin | 4871 -> 4871 bytes | |||
| -rw-r--r-- | frontend/src/images/png/logo.png (renamed from frontend/src/imgs/logo.png) | bin | 67124 -> 67124 bytes | |||
| -rw-r--r-- | frontend/src/index.js | 8 | ||||
| -rw-r--r-- | frontend/src/index.tsx | 17 | ||||
| -rw-r--r-- | frontend/src/pages/Games.tsx | 51 | ||||
| -rw-r--r-- | frontend/src/pages/Maps.tsx | 91 | ||||
| -rw-r--r-- | frontend/src/pages/Profile.tsx | 326 | ||||
| -rw-r--r-- | frontend/src/pages/User.tsx | 320 | ||||
| -rw-r--r-- | frontend/src/react-app-env.d.ts | 2 | ||||
| -rw-r--r-- | frontend/src/types/Content.tsx | 18 | ||||
| -rw-r--r-- | frontend/src/types/Game.tsx | 37 | ||||
| -rw-r--r-- | frontend/src/types/Map.tsx | 103 | ||||
| -rw-r--r-- | frontend/src/types/Pagination.tsx | 6 | ||||
| -rw-r--r-- | frontend/src/types/Profile.tsx | 63 | ||||
| -rw-r--r-- | frontend/src/types/Search.tsx | 13 | ||||
| -rw-r--r-- | frontend/src/utils/Time.tsx | 42 |
72 files changed, 4268 insertions, 3549 deletions
diff --git a/frontend/src/App.css b/frontend/src/App.css index 65e35de..3b732f0 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css | |||
| @@ -1,3 +1,20 @@ | |||
| 1 | main { | ||
| 2 | overflow: auto; | ||
| 3 | overflow-x: hidden; | ||
| 4 | position: relative; | ||
| 5 | |||
| 6 | width: calc(100% - 380px); | ||
| 7 | height: 100vh; | ||
| 8 | left: 350px; | ||
| 9 | |||
| 10 | padding-right: 30px; | ||
| 11 | |||
| 12 | font-size: 40px; | ||
| 13 | font-family: BarlowSemiCondensed-Regular; | ||
| 14 | color: #cdcfdf; | ||
| 15 | |||
| 16 | } | ||
| 17 | |||
| 1 | body { | 18 | body { |
| 2 | overflow: hidden; | 19 | overflow: hidden; |
| 3 | background-color: #141520; | 20 | background-color: #141520; |
diff --git a/frontend/src/App.js b/frontend/src/App.js deleted file mode 100644 index d96fa88..0000000 --- a/frontend/src/App.js +++ /dev/null | |||
| @@ -1,49 +0,0 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { BrowserRouter, Routes, Route} from "react-router-dom"; | ||
| 3 | |||
| 4 | import Sidebar from "./components/sidebar.js" | ||
| 5 | import Main from "./components/main.js" | ||
| 6 | import "./App.css"; | ||
| 7 | |||
| 8 | import Summary from "./components/pages/summary.js" | ||
| 9 | import Profile from "./components/pages/profile.js" | ||
| 10 | import About from './components/pages/about.js'; | ||
| 11 | import Games from "./components/pages/games.js"; | ||
| 12 | import Maplist from './components/pages/maplist.js'; | ||
| 13 | import Home from "./components/pages/maplist.js"; | ||
| 14 | import Homepage from './components/pages/home.js'; | ||
| 15 | |||
| 16 | |||
| 17 | export default function App() { | ||
| 18 | const [token, setToken] = React.useState(null); | ||
| 19 | const [mod,setMod] = React.useState(false) | ||
| 20 | React.useEffect(()=>{ | ||
| 21 | if(token!==null){ | ||
| 22 | setMod(JSON.parse(atob(token.split(".")[1])).mod) | ||
| 23 | } | ||
| 24 | },[token]) | ||
| 25 | |||
| 26 | return ( | ||
| 27 | <> | ||
| 28 | <BrowserRouter> | ||
| 29 | <Sidebar token={token} setToken={setToken}/> | ||
| 30 | <Routes> | ||
| 31 | <Route index element={<Homepage token={token} mod={mod} setToken={setToken}/>}></Route> | ||
| 32 | <Route path="/news" element={<Main text="News"/>}></Route> | ||
| 33 | <Route path="/records" element={<Main text="Records"/>}></Route> | ||
| 34 | <Route path="/leaderboards" element={<Main text="Leaderboards"/>}></Route> | ||
| 35 | <Route path="/discussions" element={<Main text="Discussion"/>}></Route> | ||
| 36 | <Route path="/scorelog" element={<Main text="Score logs"/>}></Route> | ||
| 37 | <Route path="/profile" element={<Profile token={token}/>}></Route> | ||
| 38 | <Route path="/users/*" element={<Profile/>}></Route> | ||
| 39 | <Route path="/rules" element={<Main text="Rules"/>}></Route> | ||
| 40 | <Route path="/about" element={<About/>}></Route> | ||
| 41 | <Route path="/maps/*" element={<Summary token={token} mod={mod}/>}></Route> | ||
| 42 | <Route path="/games" element={<Games/>}></Route> | ||
| 43 | <Route path="/games/*" element={<Maplist token={token} mod={mod} />}></Route> | ||
| 44 | <Route path="*" element={<Main text="404 Page not found"/>}></Route> | ||
| 45 | </Routes> | ||
| 46 | </BrowserRouter> | ||
| 47 | </> | ||
| 48 | ) | ||
| 49 | } \ No newline at end of file | ||
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..555ce4c --- /dev/null +++ b/frontend/src/App.tsx | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { Routes, Route } from "react-router-dom"; | ||
| 3 | |||
| 4 | import { UserProfile } from './types/Profile'; | ||
| 5 | import Sidebar from './components/Sidebar'; | ||
| 6 | import "./App.css"; | ||
| 7 | |||
| 8 | import Profile from './pages/Profile'; | ||
| 9 | import Games from './pages/Games'; | ||
| 10 | import Maps from './pages/Maps'; | ||
| 11 | import User from './pages/User'; | ||
| 12 | |||
| 13 | |||
| 14 | const App: React.FC = () => { | ||
| 15 | const [token, setToken] = React.useState<string | undefined>(undefined); | ||
| 16 | const [profile, setProfile] = React.useState<UserProfile | undefined>(undefined); | ||
| 17 | const [isModerator, setIsModerator] = React.useState<boolean>(true); | ||
| 18 | |||
| 19 | // React.useEffect(() => { | ||
| 20 | // if (token) { | ||
| 21 | // setIsModerator(JSON.parse(atob(token.split(".")[1])).mod) | ||
| 22 | // } | ||
| 23 | // }, [token]); | ||
| 24 | |||
| 25 | return ( | ||
| 26 | <> | ||
| 27 | <Sidebar setToken={setToken} profile={profile} setProfile={setProfile} /> | ||
| 28 | <Routes> | ||
| 29 | <Route path="/" element={<div>yo</div>} /> | ||
| 30 | <Route path="/profile" element={<Profile profile={profile!} />} /> | ||
| 31 | <Route path="/users/*" element={<User />} /> | ||
| 32 | <Route path="/games" element={<Games />} /> | ||
| 33 | <Route path="/maps/*" element={<Maps isModerator={isModerator} />} /> | ||
| 34 | <Route path="*" element={"404"} /> | ||
| 35 | </Routes> | ||
| 36 | </> | ||
| 37 | ); | ||
| 38 | }; | ||
| 39 | |||
| 40 | export default App; | ||
diff --git a/frontend/src/api/Api.tsx b/frontend/src/api/Api.tsx new file mode 100644 index 0000000..9e45bc4 --- /dev/null +++ b/frontend/src/api/Api.tsx | |||
| @@ -0,0 +1,157 @@ | |||
| 1 | import axios from 'axios'; | ||
| 2 | |||
| 3 | import { Game } from '../types/Game'; | ||
| 4 | import { MapDiscussion, MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map'; | ||
| 5 | import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '../types/Content'; | ||
| 6 | import { Search } from '../types/Search'; | ||
| 7 | import { UserProfile } from '../types/Profile'; | ||
| 8 | |||
| 9 | // add new api call function entries here | ||
| 10 | // example usage: API.get_games(); | ||
| 11 | export const API = { | ||
| 12 | user_logout: () => user_logout(), | ||
| 13 | |||
| 14 | get_user: (user_id: string) => get_user(user_id), | ||
| 15 | get_games: () => get_games(), | ||
| 16 | get_search: (q: string) => get_search(q), | ||
| 17 | get_map_summary: (map_id: string) => get_map_summary(map_id), | ||
| 18 | get_map_leaderboard: (map_id: string) => get_map_leaderboard(map_id), | ||
| 19 | get_map_discussions: (map_id: string) => get_map_discussions(map_id), | ||
| 20 | get_map_discussion: (map_id: string, discussion_id: number) => get_map_discussion(map_id, discussion_id), | ||
| 21 | |||
| 22 | post_map_summary: (map_id: string, content: ModMenuContent) => post_map_summary(map_id, content), | ||
| 23 | post_map_discussion: (map_id: string, content: MapDiscussionContent) => post_map_discussion(map_id, content), | ||
| 24 | post_map_discussion_comment: (map_id: string, discussion_id: number, content: MapDiscussionCommentContent) => post_map_discussion_comment(map_id, discussion_id, content), | ||
| 25 | |||
| 26 | put_map_image: (map_id: string, image: string) => put_map_image(map_id, image), | ||
| 27 | put_map_summary: (map_id: string, content: ModMenuContent) => put_map_summary(map_id, content), | ||
| 28 | |||
| 29 | delete_map_summary: (map_id: string, route_id: number) => delete_map_summary(map_id, route_id), | ||
| 30 | delete_map_discussion: (map_id: string, discussion_id: number) => delete_map_discussion(map_id, discussion_id), | ||
| 31 | }; | ||
| 32 | |||
| 33 | const BASE_API_URL: string = "https://lp.ardapektezol.com/api/v1/" | ||
| 34 | |||
| 35 | function url(path: string): string { | ||
| 36 | return BASE_API_URL + path; | ||
| 37 | } | ||
| 38 | |||
| 39 | // USER | ||
| 40 | |||
| 41 | const user_logout = async () => { | ||
| 42 | await axios.delete(url("token")); | ||
| 43 | }; | ||
| 44 | |||
| 45 | const get_user = async (user_id: string): Promise<UserProfile> => { | ||
| 46 | const response = await axios.get(url(`users/${user_id}`)) | ||
| 47 | return response.data.data; | ||
| 48 | }; | ||
| 49 | |||
| 50 | |||
| 51 | // GAMES | ||
| 52 | |||
| 53 | const get_games = async (): Promise<Game[]> => { | ||
| 54 | const response = await axios.get(url("games")) | ||
| 55 | return response.data.data; | ||
| 56 | }; | ||
| 57 | |||
| 58 | // SEARCH | ||
| 59 | |||
| 60 | const get_search = async (q: string): Promise<Search> => { | ||
| 61 | const response = await axios.get(url(`search?q=${q}`)) | ||
| 62 | return response.data.data; | ||
| 63 | }; | ||
| 64 | |||
| 65 | // MAP SUMMARY | ||
| 66 | |||
| 67 | const put_map_image = async (map_id: string, image: string): Promise<boolean> => { | ||
| 68 | const response = await axios.put(url(`maps/${map_id}/image`), { | ||
| 69 | "image": image, | ||
| 70 | }); | ||
| 71 | return response.data.success; | ||
| 72 | }; | ||
| 73 | |||
| 74 | const get_map_summary = async (map_id: string): Promise<MapSummary> => { | ||
| 75 | const response = await axios.get(url(`maps/${map_id}/summary`)) | ||
| 76 | return response.data.data; | ||
| 77 | }; | ||
| 78 | |||
| 79 | const post_map_summary = async (map_id: string, content: ModMenuContent): Promise<boolean> => { | ||
| 80 | const response = await axios.post(url(`maps/${map_id}/summary`), { | ||
| 81 | "user_name": content.name, | ||
| 82 | "score_count": content.score, | ||
| 83 | "record_date": content.date, | ||
| 84 | "showcase": content.showcase, | ||
| 85 | "description": content.description, | ||
| 86 | }); | ||
| 87 | return response.data.success; | ||
| 88 | }; | ||
| 89 | |||
| 90 | const put_map_summary = async (map_id: string, content: ModMenuContent): Promise<boolean> => { | ||
| 91 | const response = await axios.put(url(`maps/${map_id}/summary`), { | ||
| 92 | "route_id": content.id, | ||
| 93 | "user_name": content.name, | ||
| 94 | "score_count": content.score, | ||
| 95 | "record_date": content.date, | ||
| 96 | "showcase": content.showcase, | ||
| 97 | "description": content.description, | ||
| 98 | }); | ||
| 99 | return response.data.success; | ||
| 100 | }; | ||
| 101 | |||
| 102 | const delete_map_summary = async (map_id: string, route_id: number): Promise<boolean> => { | ||
| 103 | const response = await axios.delete(url(`maps/${map_id}/summary`), { | ||
| 104 | data: { | ||
| 105 | "route_id": route_id, | ||
| 106 | } | ||
| 107 | }); | ||
| 108 | return response.data.success; | ||
| 109 | }; | ||
| 110 | |||
| 111 | // MAP LEADERBOARDS | ||
| 112 | |||
| 113 | const get_map_leaderboard = async (map_id: string): Promise<MapLeaderboard | undefined> => { | ||
| 114 | const response = await axios.get(url(`maps/${map_id}/leaderboards`)) | ||
| 115 | if (!response.data.success) { | ||
| 116 | return undefined; | ||
| 117 | } | ||
| 118 | return response.data.data; | ||
| 119 | }; | ||
| 120 | |||
| 121 | // MAP DISCUSSIONS | ||
| 122 | |||
| 123 | const get_map_discussions = async (map_id: string): Promise<MapDiscussions | undefined> => { | ||
| 124 | const response = await axios.get(url(`maps/${map_id}/discussions`)); | ||
| 125 | if (!response.data.data.discussions) { | ||
| 126 | return undefined; | ||
| 127 | } | ||
| 128 | return response.data.data; | ||
| 129 | }; | ||
| 130 | |||
| 131 | const get_map_discussion = async (map_id: string, discussion_id: number): Promise<MapDiscussion | undefined> => { | ||
| 132 | const response = await axios.get(url(`maps/${map_id}/discussions/${discussion_id}`)); | ||
| 133 | if (!response.data.data.discussion) { | ||
| 134 | return undefined; | ||
| 135 | } | ||
| 136 | return response.data.data; | ||
| 137 | }; | ||
| 138 | |||
| 139 | const post_map_discussion = async (map_id: string, content: MapDiscussionContent): Promise<boolean> => { | ||
| 140 | const response = await axios.post(url(`maps/${map_id}/discussions`), { | ||
| 141 | "title": content.title, | ||
| 142 | "content": content.content, | ||
| 143 | }); | ||
| 144 | return response.data.success; | ||
| 145 | }; | ||
| 146 | |||
| 147 | const post_map_discussion_comment = async (map_id: string, discussion_id: number, content: MapDiscussionCommentContent): Promise<boolean> => { | ||
| 148 | const response = await axios.post(url(`maps/${map_id}/discussions/${discussion_id}`), { | ||
| 149 | "comment": content.comment, | ||
| 150 | }); | ||
| 151 | return response.data.success; | ||
| 152 | }; | ||
| 153 | |||
| 154 | const delete_map_discussion = async (map_id: string, discussion_id: number): Promise<boolean> => { | ||
| 155 | const response = await axios.delete(url(`maps/${map_id}/discussions/${discussion_id}`)); | ||
| 156 | return response.data.success; | ||
| 157 | }; | ||
diff --git a/frontend/src/components/Discussions.tsx b/frontend/src/components/Discussions.tsx new file mode 100644 index 0000000..1cd3523 --- /dev/null +++ b/frontend/src/components/Discussions.tsx | |||
| @@ -0,0 +1,151 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | |||
| 3 | import { MapDiscussion, MapDiscussions, MapDiscussionsDetail } from '../types/Map'; | ||
| 4 | import { MapDiscussionCommentContent, MapDiscussionContent } from '../types/Content'; | ||
| 5 | import { time_ago } from '../utils/Time'; | ||
| 6 | import { API } from '../api/Api'; | ||
| 7 | import "../css/Maps.css" | ||
| 8 | |||
| 9 | interface DiscussionsProps { | ||
| 10 | data?: MapDiscussions; | ||
| 11 | isModerator: boolean; | ||
| 12 | mapID: string; | ||
| 13 | onRefresh: () => void; | ||
| 14 | } | ||
| 15 | |||
| 16 | const Discussions: React.FC<DiscussionsProps> = ({ data, isModerator, mapID, onRefresh }) => { | ||
| 17 | |||
| 18 | const [discussionThread, setDiscussionThread] = React.useState<MapDiscussion | undefined>(undefined); | ||
| 19 | const [discussionSearch, setDiscussionSearch] = React.useState<string>(""); | ||
| 20 | |||
| 21 | const [createDiscussion, setCreateDiscussion] = React.useState<boolean>(false); | ||
| 22 | const [createDiscussionContent, setCreateDiscussionContent] = React.useState<MapDiscussionContent>({ | ||
| 23 | title: "", | ||
| 24 | content: "", | ||
| 25 | }); | ||
| 26 | const [createDiscussionCommentContent, setCreateDiscussionCommentContent] = React.useState<MapDiscussionCommentContent>({ | ||
| 27 | comment: "", | ||
| 28 | }); | ||
| 29 | |||
| 30 | const _open_map_discussion = async (discussion_id: number) => { | ||
| 31 | const mapDiscussion = await API.get_map_discussion(mapID, discussion_id); | ||
| 32 | setDiscussionThread(mapDiscussion); | ||
| 33 | }; | ||
| 34 | |||
| 35 | const _create_map_discussion = async () => { | ||
| 36 | await API.post_map_discussion(mapID, createDiscussionContent); | ||
| 37 | setCreateDiscussion(false); | ||
| 38 | onRefresh(); | ||
| 39 | }; | ||
| 40 | |||
| 41 | const _create_map_discussion_comment = async (discussion_id: number) => { | ||
| 42 | await API.post_map_discussion_comment(mapID, discussion_id, createDiscussionCommentContent); | ||
| 43 | await _open_map_discussion(discussion_id); | ||
| 44 | }; | ||
| 45 | |||
| 46 | const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => { | ||
| 47 | if (window.confirm(`Are you sure you want to remove post: ${discussion.title}?`)) { | ||
| 48 | await API.delete_map_discussion(mapID, discussion.id); | ||
| 49 | onRefresh(); | ||
| 50 | } | ||
| 51 | }; | ||
| 52 | |||
| 53 | return ( | ||
| 54 | <section id='section7' className='summary3'> | ||
| 55 | <div id='discussion-search'> | ||
| 56 | <input type="text" value={discussionSearch} placeholder={"Search for posts..."} onChange={(e) => setDiscussionSearch(e.target.value)} /> | ||
| 57 | <div><button onClick={() => setCreateDiscussion(true)}>New Post</button></div> | ||
| 58 | </div> | ||
| 59 | |||
| 60 | { // janky ternary operators here, could divide them to more components? | ||
| 61 | createDiscussion ? | ||
| 62 | ( | ||
| 63 | <div id='discussion-create'> | ||
| 64 | <span>Create Post</span> | ||
| 65 | <button onClick={() => setCreateDiscussion(false)}>X</button> | ||
| 66 | <div style={{ gridColumn: "1 / span 2" }}> | ||
| 67 | <input id='discussion-create-title' placeholder='Title...' onChange={(e) => setCreateDiscussionContent({ | ||
| 68 | ...createDiscussionContent, | ||
| 69 | title: e.target.value, | ||
| 70 | })} /> | ||
| 71 | <input id='discussion-create-content' placeholder='Enter the comment...' onChange={(e) => setCreateDiscussionContent({ | ||
| 72 | ...createDiscussionContent, | ||
| 73 | title: e.target.value, | ||
| 74 | })} /> | ||
| 75 | </div> | ||
| 76 | <div style={{ placeItems: "end", gridColumn: "1 / span 2" }}> | ||
| 77 | <button id='discussion-create-button' onClick={() => _create_map_discussion()}>Post</button> | ||
| 78 | </div> | ||
| 79 | </div> | ||
| 80 | ) | ||
| 81 | : | ||
| 82 | discussionThread ? | ||
| 83 | ( | ||
| 84 | <div id='discussion-thread'> | ||
| 85 | <div> | ||
| 86 | <span>{discussionThread.discussion.title}</span> | ||
| 87 | <button onClick={() => setDiscussionThread(undefined)}>X</button> | ||
| 88 | </div> | ||
| 89 | |||
| 90 | <div> | ||
| 91 | <img src={discussionThread.discussion.creator.avatar_link} alt="" /> | ||
| 92 | <div> | ||
| 93 | <span>{discussionThread.discussion.creator.user_name}</span> | ||
| 94 | <span>{time_ago(new Date(discussionThread.discussion.created_at.replace("T", " ").replace("Z", "")))}</span> | ||
| 95 | <span>{discussionThread.discussion.content}</span> | ||
| 96 | </div> | ||
| 97 | {discussionThread.discussion.comments ? | ||
| 98 | discussionThread.discussion.comments.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) | ||
| 99 | .map(e => ( | ||
| 100 | <> | ||
| 101 | <img src={e.user.avatar_link} alt="" /> | ||
| 102 | <div> | ||
| 103 | <span>{e.user.user_name}</span> | ||
| 104 | <span>{time_ago(new Date(e.date.replace("T", " ").replace("Z", "")))}</span> | ||
| 105 | <span>{e.comment}</span> | ||
| 106 | </div> | ||
| 107 | </> | ||
| 108 | )) : "" | ||
| 109 | } | ||
| 110 | </div> | ||
| 111 | <div id='discussion-send'> | ||
| 112 | <input type="text" placeholder={"Message"} onKeyDown={(e) => e.key === "Enter" && _create_map_discussion_comment(discussionThread.discussion.id)} onChange={(e) => setCreateDiscussionCommentContent({ | ||
| 113 | ...createDiscussionContent, | ||
| 114 | comment: e.target.value, | ||
| 115 | })} /> | ||
| 116 | <div><button onClick={() => _create_map_discussion_comment(discussionThread.discussion.id)}>Send</button></div> | ||
| 117 | </div> | ||
| 118 | |||
| 119 | </div> | ||
| 120 | ) | ||
| 121 | : | ||
| 122 | ( | ||
| 123 | data ? | ||
| 124 | (<> | ||
| 125 | {data.discussions.filter(f => f.title.includes(discussionSearch)).sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) | ||
| 126 | .map((e, i) => ( | ||
| 127 | <div id='discussion-post'> | ||
| 128 | <button key={e.id} onClick={() => _open_map_discussion(e.id)}> | ||
| 129 | <span>{e.title}</span> | ||
| 130 | {isModerator ? | ||
| 131 | <button onClick={(m) => { | ||
| 132 | m.stopPropagation(); | ||
| 133 | _delete_map_discussion(e); | ||
| 134 | }}>Delete Post</button> | ||
| 135 | : <span></span> | ||
| 136 | } | ||
| 137 | <span><b>{e.creator.user_name}:</b> {e.content}</span> | ||
| 138 | <span>Last Updated: {time_ago(new Date(e.updated_at.replace("T", " ").replace("Z", "")))}</span> | ||
| 139 | </button> | ||
| 140 | </div> | ||
| 141 | ))} | ||
| 142 | </>) | ||
| 143 | : | ||
| 144 | (<span style={{ textAlign: "center", display: "block" }}>No Discussions...</span>) | ||
| 145 | ) | ||
| 146 | } | ||
| 147 | </section> | ||
| 148 | ); | ||
| 149 | }; | ||
| 150 | |||
| 151 | export default Discussions; | ||
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx new file mode 100644 index 0000000..8e58ce9 --- /dev/null +++ b/frontend/src/components/GameEntry.tsx | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { Link } from "react-router-dom"; | ||
| 3 | |||
| 4 | import { Game } from '../types/Game'; | ||
| 5 | import "../css/Games.css" | ||
| 6 | |||
| 7 | interface GameEntryProps { | ||
| 8 | game: Game; | ||
| 9 | } | ||
| 10 | |||
| 11 | const GameEntry: React.FC<GameEntryProps> = ({ game }) => { | ||
| 12 | |||
| 13 | React.useEffect(() => { | ||
| 14 | game.category_portals.forEach(catInfo => { | ||
| 15 | const itemBody = document.createElement("div"); | ||
| 16 | const itemTitle = document.createElement("span"); | ||
| 17 | const spacing = document.createElement("br"); | ||
| 18 | const itemNum = document.createElement("span"); | ||
| 19 | |||
| 20 | itemTitle.innerText = catInfo.category.name; | ||
| 21 | itemNum.innerText = catInfo.portal_count as any as string; | ||
| 22 | itemTitle.classList.add("games-page-item-body-item-title"); | ||
| 23 | itemNum.classList.add("games-page-item-body-item-num"); | ||
| 24 | itemBody.appendChild(itemTitle); | ||
| 25 | itemBody.appendChild(spacing); | ||
| 26 | itemBody.appendChild(itemNum); | ||
| 27 | itemBody.className = "games-page-item-body-item"; | ||
| 28 | |||
| 29 | // itemBody.innerHTML = ` | ||
| 30 | // <span className='games-page-item-body-item-title'>${catInfo.category.name}</span><br /> | ||
| 31 | // <span className='games-page-item-body-item-num'>${catInfo.portal_count}</span>` | ||
| 32 | |||
| 33 | document.getElementById(`${game.id}`)!.appendChild(itemBody); | ||
| 34 | }); | ||
| 35 | }, []); | ||
| 36 | |||
| 37 | return ( | ||
| 38 | <Link to={"/games/" + game.id}><div className='games-page-item'> | ||
| 39 | <div className='games-page-item-header'> | ||
| 40 | <div style={{ backgroundImage: `url(${game.image})` }} className='games-page-item-header-img'></div> | ||
| 41 | <span><b>{game.name}</b></span> | ||
| 42 | </div> | ||
| 43 | <div id={game.id as any as string} className='games-page-item-body'> | ||
| 44 | </div> | ||
| 45 | </div></Link> | ||
| 46 | ); | ||
| 47 | }; | ||
| 48 | |||
| 49 | export default GameEntry; | ||
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx new file mode 100644 index 0000000..badff37 --- /dev/null +++ b/frontend/src/components/Leaderboards.tsx | |||
| @@ -0,0 +1,105 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | |||
| 3 | import { DownloadIcon, ThreedotIcon } from '../images/Images'; | ||
| 4 | import { MapLeaderboard } from '../types/Map'; | ||
| 5 | import { ticks_to_time, time_ago } from '../utils/Time'; | ||
| 6 | import "../css/Maps.css" | ||
| 7 | |||
| 8 | interface LeaderboardsProps { | ||
| 9 | data?: MapLeaderboard; | ||
| 10 | } | ||
| 11 | |||
| 12 | const Leaderboards: React.FC<LeaderboardsProps> = ({ data }) => { | ||
| 13 | |||
| 14 | const [pageNumber, setPageNumber] = React.useState<number>(1); | ||
| 15 | |||
| 16 | if (!data) { | ||
| 17 | return ( | ||
| 18 | <section id='section6' className='summary2'> | ||
| 19 | <h1 style={{ textAlign: "center" }}>Map is not available for competitive boards.</h1> | ||
| 20 | </section> | ||
| 21 | ); | ||
| 22 | }; | ||
| 23 | |||
| 24 | if (data.records.length === 0) { | ||
| 25 | return ( | ||
| 26 | <section id='section6' className='summary2'> | ||
| 27 | <h1 style={{ textAlign: "center" }}>No records found.</h1> | ||
| 28 | </section> | ||
| 29 | ); | ||
| 30 | }; | ||
| 31 | |||
| 32 | return ( | ||
| 33 | <section id='section6' className='summary2'> | ||
| 34 | |||
| 35 | <div id='leaderboard-top' | ||
| 36 | style={data.map.is_coop ? { gridTemplateColumns: "7.5% 40% 7.5% 15% 15% 15%" } : { gridTemplateColumns: "7.5% 30% 10% 20% 17.5% 15%" }} | ||
| 37 | > | ||
| 38 | <span>Place</span> | ||
| 39 | |||
| 40 | {data.map.is_coop ? ( | ||
| 41 | <div id='runner'> | ||
| 42 | <span>Host</span> | ||
| 43 | <span>Partner</span> | ||
| 44 | </div> | ||
| 45 | ) : ( | ||
| 46 | <span>Runner</span> | ||
| 47 | )} | ||
| 48 | |||
| 49 | <span>Portals</span> | ||
| 50 | <span>Time</span> | ||
| 51 | <span>Date</span> | ||
| 52 | <div id='page-number'> | ||
| 53 | <div> | ||
| 54 | |||
| 55 | <button onClick={() => pageNumber === 1 ? null : setPageNumber(prevPageNumber => prevPageNumber - 1)} | ||
| 56 | ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> | ||
| 57 | <span>{data.pagination.current_page}/{data.pagination.total_pages}</span> | ||
| 58 | <button onClick={() => pageNumber === data.pagination.total_pages ? null : setPageNumber(prevPageNumber => prevPageNumber + 1)} | ||
| 59 | ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> | ||
| 60 | </div> | ||
| 61 | </div> | ||
| 62 | </div> | ||
| 63 | <hr /> | ||
| 64 | <div id='leaderboard-records'> | ||
| 65 | {data.records.map((r, index) => ( | ||
| 66 | <span className='leaderboard-record' key={index} | ||
| 67 | style={data.map.is_coop ? { gridTemplateColumns: "3% 4.5% 40% 4% 3.5% 15% 15% 14.5%" } : { gridTemplateColumns: "3% 4.5% 30% 4% 6% 20% 17% 15%" }} | ||
| 68 | > | ||
| 69 | <span>{r.placement}</span> | ||
| 70 | <span> </span> | ||
| 71 | {r.kind === "multiplayer" ? ( | ||
| 72 | <div> | ||
| 73 | <span><img src={r.host.avatar_link} alt='' /> {r.host.user_name}</span> | ||
| 74 | <span><img src={r.partner.avatar_link} alt='' /> {r.partner.user_name}</span> | ||
| 75 | </div> | ||
| 76 | ) : ( | ||
| 77 | <div><span><img src={r.user.avatar_link} alt='' /> {r.user.user_name}</span></div> | ||
| 78 | )} | ||
| 79 | |||
| 80 | <span>{r.score_count}</span> | ||
| 81 | <span> </span> | ||
| 82 | <span className='hover-popup' popup-text={(r.score_time) + " ticks"}>{ticks_to_time(r.score_time)}</span> | ||
| 83 | <span className='hover-popup' popup-text={r.record_date.replace("T", ' ').split(".")[0]}>{time_ago(new Date(r.record_date.replace("T", " ").replace("Z", "")))}</span> | ||
| 84 | |||
| 85 | {r.kind === "multiplayer" ? ( | ||
| 86 | <span> | ||
| 87 | <button onClick={() => { window.alert(`Host demo ID: ${r.host_demo_id} \nParnter demo ID: ${r.partner_demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | ||
| 88 | <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${r.partner_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(160deg) contrast(60%) saturate(1000%)" }} /></button> | ||
| 89 | <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${r.host_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(300deg) contrast(60%) saturate(1000%)" }} /></button> | ||
| 90 | </span> | ||
| 91 | ) : ( | ||
| 92 | |||
| 93 | <span> | ||
| 94 | <button onClick={() => { window.alert(`Demo ID: ${r.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | ||
| 95 | <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${r.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | ||
| 96 | </span> | ||
| 97 | )} | ||
| 98 | </span> | ||
| 99 | ))} | ||
| 100 | </div> | ||
| 101 | </section> | ||
| 102 | ); | ||
| 103 | }; | ||
| 104 | |||
| 105 | export default Leaderboards; | ||
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx new file mode 100644 index 0000000..adfa718 --- /dev/null +++ b/frontend/src/components/Login.tsx | |||
| @@ -0,0 +1,1931 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { Link, useNavigate } from 'react-router-dom'; | ||
| 3 | |||
| 4 | import { ExitIcon, UserIcon, LoginIcon } from '../images/Images'; | ||
| 5 | import { UserProfile } from '../types/Profile'; | ||
| 6 | import { API } from '../api/Api'; | ||
| 7 | import "../css/Login.css"; | ||
| 8 | |||
| 9 | interface LoginProps { | ||
| 10 | token?: string; | ||
| 11 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; | ||
| 12 | profile?: UserProfile; | ||
| 13 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; | ||
| 14 | }; | ||
| 15 | |||
| 16 | const Login: React.FC<LoginProps> = ({ token, setToken, profile, setProfile }) => { | ||
| 17 | |||
| 18 | const navigate = useNavigate(); | ||
| 19 | |||
| 20 | const _logout = () => { | ||
| 21 | setProfile(undefined); | ||
| 22 | setToken(undefined); | ||
| 23 | API.user_logout(); | ||
| 24 | navigate("/"); | ||
| 25 | } | ||
| 26 | |||
| 27 | return ( | ||
| 28 | <> | ||
| 29 | {profile | ||
| 30 | ? | ||
| 31 | ( | ||
| 32 | <> | ||
| 33 | <Link to="/profile" tabIndex={-1} className='login'> | ||
| 34 | <button className='sidebar-button'> | ||
| 35 | <img src={profile.avatar_link} alt="" /> | ||
| 36 | <span>{profile.user_name}</span> | ||
| 37 | </button> | ||
| 38 | <button className='sidebar-button' onClick={_logout}> | ||
| 39 | <img src={ExitIcon} alt="" /><span></span> | ||
| 40 | </button> | ||
| 41 | </Link> | ||
| 42 | </> | ||
| 43 | ) | ||
| 44 | : | ||
| 45 | ( | ||
| 46 | <Link to="/api/v1/login" tabIndex={-1} className='login' > | ||
| 47 | <button className='sidebar-button' onClick={() => { | ||
| 48 | setProfile({ | ||
| 49 | "profile": true, | ||
| 50 | "steam_id": "76561198131629989", | ||
| 51 | "user_name": "BiSaXa", | ||
| 52 | "avatar_link": "https://avatars.steamstatic.com/fa7f64c79b247c8a80cafbd6dd8033b98cc1153c_full.jpg", | ||
| 53 | "country_code": "TR", | ||
| 54 | "titles": [ | ||
| 55 | { | ||
| 56 | "name": "Admin", | ||
| 57 | "color": "ce6000" | ||
| 58 | }, | ||
| 59 | { | ||
| 60 | "name": "Moderator", | ||
| 61 | "color": "4a8b00" | ||
| 62 | } | ||
| 63 | ], | ||
| 64 | "links": { | ||
| 65 | "p2sr": "-", | ||
| 66 | "steam": "-", | ||
| 67 | "youtube": "-", | ||
| 68 | "twitch": "-" | ||
| 69 | }, | ||
| 70 | "rankings": { | ||
| 71 | "overall": { | ||
| 72 | "rank": 1, | ||
| 73 | "completion_count": 4, | ||
| 74 | "completion_total": 105 | ||
| 75 | }, | ||
| 76 | "singleplayer": { | ||
| 77 | "rank": 1, | ||
| 78 | "completion_count": 3, | ||
| 79 | "completion_total": 57 | ||
| 80 | }, | ||
| 81 | "cooperative": { | ||
| 82 | "rank": 1, | ||
| 83 | "completion_count": 1, | ||
| 84 | "completion_total": 48 | ||
| 85 | } | ||
| 86 | }, | ||
| 87 | "records": [ | ||
| 88 | { | ||
| 89 | "game_id": 1, | ||
| 90 | "category_id": 1, | ||
| 91 | "map_id": 3, | ||
| 92 | "map_name": "Portal Gun", | ||
| 93 | "map_wr_count": 0, | ||
| 94 | "placement": 1, | ||
| 95 | "scores": [ | ||
| 96 | { | ||
| 97 | "record_id": 350, | ||
| 98 | "demo_id": "e9ec0b83-7b95-4fa9-b974-2245fb79d5ca", | ||
| 99 | "score_count": 0, | ||
| 100 | "score_time": 3968, | ||
| 101 | "date": "2023-09-23T14:57:35.430781Z" | ||
| 102 | }, | ||
| 103 | { | ||
| 104 | "record_id": 282, | ||
| 105 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 106 | "score_count": 31, | ||
| 107 | "score_time": 9999, | ||
| 108 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 109 | } | ||
| 110 | ] | ||
| 111 | }, | ||
| 112 | { | ||
| 113 | "game_id": 1, | ||
| 114 | "category_id": 1, | ||
| 115 | "map_id": 4, | ||
| 116 | "map_name": "Smooth Jazz", | ||
| 117 | "map_wr_count": 0, | ||
| 118 | "placement": 1, | ||
| 119 | "scores": [ | ||
| 120 | { | ||
| 121 | "record_id": 283, | ||
| 122 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 123 | "score_count": 31, | ||
| 124 | "score_time": 9999, | ||
| 125 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 126 | } | ||
| 127 | ] | ||
| 128 | }, | ||
| 129 | { | ||
| 130 | "game_id": 1, | ||
| 131 | "category_id": 1, | ||
| 132 | "map_id": 5, | ||
| 133 | "map_name": "Cube Momentum", | ||
| 134 | "map_wr_count": 0, | ||
| 135 | "placement": 1, | ||
| 136 | "scores": [ | ||
| 137 | { | ||
| 138 | "record_id": 284, | ||
| 139 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 140 | "score_count": 31, | ||
| 141 | "score_time": 9999, | ||
| 142 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 143 | } | ||
| 144 | ] | ||
| 145 | }, | ||
| 146 | { | ||
| 147 | "game_id": 1, | ||
| 148 | "category_id": 1, | ||
| 149 | "map_id": 6, | ||
| 150 | "map_name": "Future Starter", | ||
| 151 | "map_wr_count": 0, | ||
| 152 | "placement": 1, | ||
| 153 | "scores": [ | ||
| 154 | { | ||
| 155 | "record_id": 351, | ||
| 156 | "demo_id": "d5ee2227-e195-4e8d-bd1d-746b17538df7", | ||
| 157 | "score_count": 2, | ||
| 158 | "score_time": 71378, | ||
| 159 | "date": "2023-09-23T15:11:16.579757Z" | ||
| 160 | }, | ||
| 161 | { | ||
| 162 | "record_id": 285, | ||
| 163 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 164 | "score_count": 31, | ||
| 165 | "score_time": 9999, | ||
| 166 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 167 | } | ||
| 168 | ] | ||
| 169 | }, | ||
| 170 | { | ||
| 171 | "game_id": 1, | ||
| 172 | "category_id": 1, | ||
| 173 | "map_id": 7, | ||
| 174 | "map_name": "Secret Panel", | ||
| 175 | "map_wr_count": 0, | ||
| 176 | "placement": 1, | ||
| 177 | "scores": [ | ||
| 178 | { | ||
| 179 | "record_id": 352, | ||
| 180 | "demo_id": "64ca612d-4586-40df-9cf3-850c270b5592", | ||
| 181 | "score_count": 0, | ||
| 182 | "score_time": 10943, | ||
| 183 | "date": "2023-09-23T15:19:15.413596Z" | ||
| 184 | }, | ||
| 185 | { | ||
| 186 | "record_id": 286, | ||
| 187 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 188 | "score_count": 31, | ||
| 189 | "score_time": 9999, | ||
| 190 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 191 | } | ||
| 192 | ] | ||
| 193 | }, | ||
| 194 | { | ||
| 195 | "game_id": 1, | ||
| 196 | "category_id": 1, | ||
| 197 | "map_id": 9, | ||
| 198 | "map_name": "Incinerator", | ||
| 199 | "map_wr_count": 0, | ||
| 200 | "placement": 1, | ||
| 201 | "scores": [ | ||
| 202 | { | ||
| 203 | "record_id": 287, | ||
| 204 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 205 | "score_count": 31, | ||
| 206 | "score_time": 9999, | ||
| 207 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 208 | } | ||
| 209 | ] | ||
| 210 | }, | ||
| 211 | { | ||
| 212 | "game_id": 1, | ||
| 213 | "category_id": 2, | ||
| 214 | "map_id": 10, | ||
| 215 | "map_name": "Laser Intro", | ||
| 216 | "map_wr_count": 0, | ||
| 217 | "placement": 1, | ||
| 218 | "scores": [ | ||
| 219 | { | ||
| 220 | "record_id": 288, | ||
| 221 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 222 | "score_count": 31, | ||
| 223 | "score_time": 9999, | ||
| 224 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 225 | } | ||
| 226 | ] | ||
| 227 | }, | ||
| 228 | { | ||
| 229 | "game_id": 1, | ||
| 230 | "category_id": 2, | ||
| 231 | "map_id": 11, | ||
| 232 | "map_name": "Laser Stairs", | ||
| 233 | "map_wr_count": 0, | ||
| 234 | "placement": 1, | ||
| 235 | "scores": [ | ||
| 236 | { | ||
| 237 | "record_id": 289, | ||
| 238 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 239 | "score_count": 31, | ||
| 240 | "score_time": 9999, | ||
| 241 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 242 | } | ||
| 243 | ] | ||
| 244 | }, | ||
| 245 | { | ||
| 246 | "game_id": 1, | ||
| 247 | "category_id": 2, | ||
| 248 | "map_id": 12, | ||
| 249 | "map_name": "Dual Lasers", | ||
| 250 | "map_wr_count": 0, | ||
| 251 | "placement": 1, | ||
| 252 | "scores": [ | ||
| 253 | { | ||
| 254 | "record_id": 290, | ||
| 255 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 256 | "score_count": 31, | ||
| 257 | "score_time": 9999, | ||
| 258 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 259 | } | ||
| 260 | ] | ||
| 261 | }, | ||
| 262 | { | ||
| 263 | "game_id": 1, | ||
| 264 | "category_id": 2, | ||
| 265 | "map_id": 13, | ||
| 266 | "map_name": "Laser Over Goo", | ||
| 267 | "map_wr_count": 0, | ||
| 268 | "placement": 1, | ||
| 269 | "scores": [ | ||
| 270 | { | ||
| 271 | "record_id": 291, | ||
| 272 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 273 | "score_count": 31, | ||
| 274 | "score_time": 9999, | ||
| 275 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 276 | } | ||
| 277 | ] | ||
| 278 | }, | ||
| 279 | { | ||
| 280 | "game_id": 1, | ||
| 281 | "category_id": 2, | ||
| 282 | "map_id": 14, | ||
| 283 | "map_name": "Catapult Intro", | ||
| 284 | "map_wr_count": 0, | ||
| 285 | "placement": 1, | ||
| 286 | "scores": [ | ||
| 287 | { | ||
| 288 | "record_id": 338, | ||
| 289 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 290 | "score_count": 31, | ||
| 291 | "score_time": 9999, | ||
| 292 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 293 | } | ||
| 294 | ] | ||
| 295 | }, | ||
| 296 | { | ||
| 297 | "game_id": 1, | ||
| 298 | "category_id": 2, | ||
| 299 | "map_id": 15, | ||
| 300 | "map_name": "Trust Fling", | ||
| 301 | "map_wr_count": 0, | ||
| 302 | "placement": 1, | ||
| 303 | "scores": [ | ||
| 304 | { | ||
| 305 | "record_id": 292, | ||
| 306 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 307 | "score_count": 31, | ||
| 308 | "score_time": 9999, | ||
| 309 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 310 | } | ||
| 311 | ] | ||
| 312 | }, | ||
| 313 | { | ||
| 314 | "game_id": 1, | ||
| 315 | "category_id": 2, | ||
| 316 | "map_id": 16, | ||
| 317 | "map_name": "Pit Flings", | ||
| 318 | "map_wr_count": 0, | ||
| 319 | "placement": 1, | ||
| 320 | "scores": [ | ||
| 321 | { | ||
| 322 | "record_id": 293, | ||
| 323 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 324 | "score_count": 31, | ||
| 325 | "score_time": 9999, | ||
| 326 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 327 | } | ||
| 328 | ] | ||
| 329 | }, | ||
| 330 | { | ||
| 331 | "game_id": 1, | ||
| 332 | "category_id": 2, | ||
| 333 | "map_id": 17, | ||
| 334 | "map_name": "Fizzler Intro", | ||
| 335 | "map_wr_count": 0, | ||
| 336 | "placement": 1, | ||
| 337 | "scores": [ | ||
| 338 | { | ||
| 339 | "record_id": 294, | ||
| 340 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 341 | "score_count": 31, | ||
| 342 | "score_time": 9999, | ||
| 343 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 344 | } | ||
| 345 | ] | ||
| 346 | }, | ||
| 347 | { | ||
| 348 | "game_id": 1, | ||
| 349 | "category_id": 3, | ||
| 350 | "map_id": 18, | ||
| 351 | "map_name": "Ceiling Catapult", | ||
| 352 | "map_wr_count": 0, | ||
| 353 | "placement": 1, | ||
| 354 | "scores": [ | ||
| 355 | { | ||
| 356 | "record_id": 295, | ||
| 357 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 358 | "score_count": 31, | ||
| 359 | "score_time": 9999, | ||
| 360 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 361 | } | ||
| 362 | ] | ||
| 363 | }, | ||
| 364 | { | ||
| 365 | "game_id": 1, | ||
| 366 | "category_id": 3, | ||
| 367 | "map_id": 19, | ||
| 368 | "map_name": "Ricochet", | ||
| 369 | "map_wr_count": 0, | ||
| 370 | "placement": 1, | ||
| 371 | "scores": [ | ||
| 372 | { | ||
| 373 | "record_id": 296, | ||
| 374 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 375 | "score_count": 31, | ||
| 376 | "score_time": 9999, | ||
| 377 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 378 | } | ||
| 379 | ] | ||
| 380 | }, | ||
| 381 | { | ||
| 382 | "game_id": 1, | ||
| 383 | "category_id": 3, | ||
| 384 | "map_id": 20, | ||
| 385 | "map_name": "Bridge Intro", | ||
| 386 | "map_wr_count": 0, | ||
| 387 | "placement": 1, | ||
| 388 | "scores": [ | ||
| 389 | { | ||
| 390 | "record_id": 297, | ||
| 391 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 392 | "score_count": 31, | ||
| 393 | "score_time": 9999, | ||
| 394 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 395 | } | ||
| 396 | ] | ||
| 397 | }, | ||
| 398 | { | ||
| 399 | "game_id": 1, | ||
| 400 | "category_id": 3, | ||
| 401 | "map_id": 21, | ||
| 402 | "map_name": "Bridge The Gap", | ||
| 403 | "map_wr_count": 0, | ||
| 404 | "placement": 1, | ||
| 405 | "scores": [ | ||
| 406 | { | ||
| 407 | "record_id": 298, | ||
| 408 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 409 | "score_count": 31, | ||
| 410 | "score_time": 9999, | ||
| 411 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 412 | } | ||
| 413 | ] | ||
| 414 | }, | ||
| 415 | { | ||
| 416 | "game_id": 1, | ||
| 417 | "category_id": 3, | ||
| 418 | "map_id": 22, | ||
| 419 | "map_name": "Turret Intro", | ||
| 420 | "map_wr_count": 0, | ||
| 421 | "placement": 1, | ||
| 422 | "scores": [ | ||
| 423 | { | ||
| 424 | "record_id": 299, | ||
| 425 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 426 | "score_count": 31, | ||
| 427 | "score_time": 9999, | ||
| 428 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 429 | } | ||
| 430 | ] | ||
| 431 | }, | ||
| 432 | { | ||
| 433 | "game_id": 1, | ||
| 434 | "category_id": 3, | ||
| 435 | "map_id": 23, | ||
| 436 | "map_name": "Laser Relays", | ||
| 437 | "map_wr_count": 0, | ||
| 438 | "placement": 1, | ||
| 439 | "scores": [ | ||
| 440 | { | ||
| 441 | "record_id": 300, | ||
| 442 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 443 | "score_count": 31, | ||
| 444 | "score_time": 9999, | ||
| 445 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 446 | } | ||
| 447 | ] | ||
| 448 | }, | ||
| 449 | { | ||
| 450 | "game_id": 1, | ||
| 451 | "category_id": 3, | ||
| 452 | "map_id": 24, | ||
| 453 | "map_name": "Turret Blocker", | ||
| 454 | "map_wr_count": 0, | ||
| 455 | "placement": 1, | ||
| 456 | "scores": [ | ||
| 457 | { | ||
| 458 | "record_id": 301, | ||
| 459 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 460 | "score_count": 31, | ||
| 461 | "score_time": 9999, | ||
| 462 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 463 | } | ||
| 464 | ] | ||
| 465 | }, | ||
| 466 | { | ||
| 467 | "game_id": 1, | ||
| 468 | "category_id": 3, | ||
| 469 | "map_id": 25, | ||
| 470 | "map_name": "Laser vs Turret", | ||
| 471 | "map_wr_count": 0, | ||
| 472 | "placement": 1, | ||
| 473 | "scores": [ | ||
| 474 | { | ||
| 475 | "record_id": 302, | ||
| 476 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 477 | "score_count": 31, | ||
| 478 | "score_time": 9999, | ||
| 479 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 480 | } | ||
| 481 | ] | ||
| 482 | }, | ||
| 483 | { | ||
| 484 | "game_id": 1, | ||
| 485 | "category_id": 3, | ||
| 486 | "map_id": 26, | ||
| 487 | "map_name": "Pull The Rug", | ||
| 488 | "map_wr_count": 0, | ||
| 489 | "placement": 2, | ||
| 490 | "scores": [ | ||
| 491 | { | ||
| 492 | "record_id": 303, | ||
| 493 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 494 | "score_count": 31, | ||
| 495 | "score_time": 9999, | ||
| 496 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 497 | } | ||
| 498 | ] | ||
| 499 | }, | ||
| 500 | { | ||
| 501 | "game_id": 1, | ||
| 502 | "category_id": 4, | ||
| 503 | "map_id": 27, | ||
| 504 | "map_name": "Column Blocker", | ||
| 505 | "map_wr_count": 0, | ||
| 506 | "placement": 1, | ||
| 507 | "scores": [ | ||
| 508 | { | ||
| 509 | "record_id": 304, | ||
| 510 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 511 | "score_count": 31, | ||
| 512 | "score_time": 9999, | ||
| 513 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 514 | } | ||
| 515 | ] | ||
| 516 | }, | ||
| 517 | { | ||
| 518 | "game_id": 1, | ||
| 519 | "category_id": 4, | ||
| 520 | "map_id": 28, | ||
| 521 | "map_name": "Laser Chaining", | ||
| 522 | "map_wr_count": 0, | ||
| 523 | "placement": 1, | ||
| 524 | "scores": [ | ||
| 525 | { | ||
| 526 | "record_id": 305, | ||
| 527 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 528 | "score_count": 31, | ||
| 529 | "score_time": 9999, | ||
| 530 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 531 | } | ||
| 532 | ] | ||
| 533 | }, | ||
| 534 | { | ||
| 535 | "game_id": 1, | ||
| 536 | "category_id": 4, | ||
| 537 | "map_id": 29, | ||
| 538 | "map_name": "Triple Laser", | ||
| 539 | "map_wr_count": 0, | ||
| 540 | "placement": 2, | ||
| 541 | "scores": [ | ||
| 542 | { | ||
| 543 | "record_id": 337, | ||
| 544 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 545 | "score_count": 31, | ||
| 546 | "score_time": 9999, | ||
| 547 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 548 | } | ||
| 549 | ] | ||
| 550 | }, | ||
| 551 | { | ||
| 552 | "game_id": 1, | ||
| 553 | "category_id": 4, | ||
| 554 | "map_id": 30, | ||
| 555 | "map_name": "Jail Break", | ||
| 556 | "map_wr_count": 0, | ||
| 557 | "placement": 1, | ||
| 558 | "scores": [ | ||
| 559 | { | ||
| 560 | "record_id": 306, | ||
| 561 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 562 | "score_count": 31, | ||
| 563 | "score_time": 9999, | ||
| 564 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 565 | } | ||
| 566 | ] | ||
| 567 | }, | ||
| 568 | { | ||
| 569 | "game_id": 1, | ||
| 570 | "category_id": 4, | ||
| 571 | "map_id": 31, | ||
| 572 | "map_name": "Escape", | ||
| 573 | "map_wr_count": 0, | ||
| 574 | "placement": 1, | ||
| 575 | "scores": [ | ||
| 576 | { | ||
| 577 | "record_id": 307, | ||
| 578 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 579 | "score_count": 31, | ||
| 580 | "score_time": 9999, | ||
| 581 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 582 | } | ||
| 583 | ] | ||
| 584 | }, | ||
| 585 | { | ||
| 586 | "game_id": 1, | ||
| 587 | "category_id": 5, | ||
| 588 | "map_id": 32, | ||
| 589 | "map_name": "Turret Factory", | ||
| 590 | "map_wr_count": 0, | ||
| 591 | "placement": 1, | ||
| 592 | "scores": [ | ||
| 593 | { | ||
| 594 | "record_id": 308, | ||
| 595 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 596 | "score_count": 31, | ||
| 597 | "score_time": 9999, | ||
| 598 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 599 | } | ||
| 600 | ] | ||
| 601 | }, | ||
| 602 | { | ||
| 603 | "game_id": 1, | ||
| 604 | "category_id": 5, | ||
| 605 | "map_id": 33, | ||
| 606 | "map_name": "Turret Sabotage", | ||
| 607 | "map_wr_count": 0, | ||
| 608 | "placement": 1, | ||
| 609 | "scores": [ | ||
| 610 | { | ||
| 611 | "record_id": 309, | ||
| 612 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 613 | "score_count": 31, | ||
| 614 | "score_time": 9999, | ||
| 615 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 616 | } | ||
| 617 | ] | ||
| 618 | }, | ||
| 619 | { | ||
| 620 | "game_id": 1, | ||
| 621 | "category_id": 5, | ||
| 622 | "map_id": 34, | ||
| 623 | "map_name": "Neurotoxin Sabotage", | ||
| 624 | "map_wr_count": 0, | ||
| 625 | "placement": 1, | ||
| 626 | "scores": [ | ||
| 627 | { | ||
| 628 | "record_id": 310, | ||
| 629 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 630 | "score_count": 31, | ||
| 631 | "score_time": 9999, | ||
| 632 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 633 | } | ||
| 634 | ] | ||
| 635 | }, | ||
| 636 | { | ||
| 637 | "game_id": 1, | ||
| 638 | "category_id": 5, | ||
| 639 | "map_id": 35, | ||
| 640 | "map_name": "Core", | ||
| 641 | "map_wr_count": 2, | ||
| 642 | "placement": 1, | ||
| 643 | "scores": [ | ||
| 644 | { | ||
| 645 | "record_id": 311, | ||
| 646 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 647 | "score_count": 31, | ||
| 648 | "score_time": 9999, | ||
| 649 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 650 | } | ||
| 651 | ] | ||
| 652 | }, | ||
| 653 | { | ||
| 654 | "game_id": 1, | ||
| 655 | "category_id": 6, | ||
| 656 | "map_id": 36, | ||
| 657 | "map_name": "Underground", | ||
| 658 | "map_wr_count": 0, | ||
| 659 | "placement": 1, | ||
| 660 | "scores": [ | ||
| 661 | { | ||
| 662 | "record_id": 353, | ||
| 663 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 664 | "score_count": 31, | ||
| 665 | "score_time": 9999, | ||
| 666 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 667 | } | ||
| 668 | ] | ||
| 669 | }, | ||
| 670 | { | ||
| 671 | "game_id": 1, | ||
| 672 | "category_id": 6, | ||
| 673 | "map_id": 37, | ||
| 674 | "map_name": "Cave Johnson", | ||
| 675 | "map_wr_count": 0, | ||
| 676 | "placement": 1, | ||
| 677 | "scores": [ | ||
| 678 | { | ||
| 679 | "record_id": 313, | ||
| 680 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 681 | "score_count": 31, | ||
| 682 | "score_time": 9999, | ||
| 683 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 684 | } | ||
| 685 | ] | ||
| 686 | }, | ||
| 687 | { | ||
| 688 | "game_id": 1, | ||
| 689 | "category_id": 6, | ||
| 690 | "map_id": 38, | ||
| 691 | "map_name": "Repulsion Intro", | ||
| 692 | "map_wr_count": 0, | ||
| 693 | "placement": 2, | ||
| 694 | "scores": [ | ||
| 695 | { | ||
| 696 | "record_id": 314, | ||
| 697 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 698 | "score_count": 31, | ||
| 699 | "score_time": 9999, | ||
| 700 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 701 | } | ||
| 702 | ] | ||
| 703 | }, | ||
| 704 | { | ||
| 705 | "game_id": 1, | ||
| 706 | "category_id": 6, | ||
| 707 | "map_id": 39, | ||
| 708 | "map_name": "Bomb Flings", | ||
| 709 | "map_wr_count": 0, | ||
| 710 | "placement": 1, | ||
| 711 | "scores": [ | ||
| 712 | { | ||
| 713 | "record_id": 315, | ||
| 714 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 715 | "score_count": 31, | ||
| 716 | "score_time": 9999, | ||
| 717 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 718 | } | ||
| 719 | ] | ||
| 720 | }, | ||
| 721 | { | ||
| 722 | "game_id": 1, | ||
| 723 | "category_id": 6, | ||
| 724 | "map_id": 40, | ||
| 725 | "map_name": "Crazy Box", | ||
| 726 | "map_wr_count": 0, | ||
| 727 | "placement": 1, | ||
| 728 | "scores": [ | ||
| 729 | { | ||
| 730 | "record_id": 316, | ||
| 731 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 732 | "score_count": 31, | ||
| 733 | "score_time": 9999, | ||
| 734 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 735 | } | ||
| 736 | ] | ||
| 737 | }, | ||
| 738 | { | ||
| 739 | "game_id": 1, | ||
| 740 | "category_id": 6, | ||
| 741 | "map_id": 41, | ||
| 742 | "map_name": "PotatOS", | ||
| 743 | "map_wr_count": 0, | ||
| 744 | "placement": 1, | ||
| 745 | "scores": [ | ||
| 746 | { | ||
| 747 | "record_id": 317, | ||
| 748 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 749 | "score_count": 31, | ||
| 750 | "score_time": 9999, | ||
| 751 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 752 | } | ||
| 753 | ] | ||
| 754 | }, | ||
| 755 | { | ||
| 756 | "game_id": 1, | ||
| 757 | "category_id": 7, | ||
| 758 | "map_id": 42, | ||
| 759 | "map_name": "Propulsion Intro", | ||
| 760 | "map_wr_count": 0, | ||
| 761 | "placement": 2, | ||
| 762 | "scores": [ | ||
| 763 | { | ||
| 764 | "record_id": 362, | ||
| 765 | "demo_id": "51453c2b-79a4-4fab-81bf-442cbbc997d6", | ||
| 766 | "score_count": 3, | ||
| 767 | "score_time": 856, | ||
| 768 | "date": "2023-11-06T15:45:52.867581Z" | ||
| 769 | }, | ||
| 770 | { | ||
| 771 | "record_id": 318, | ||
| 772 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 773 | "score_count": 31, | ||
| 774 | "score_time": 9999, | ||
| 775 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 776 | } | ||
| 777 | ] | ||
| 778 | }, | ||
| 779 | { | ||
| 780 | "game_id": 1, | ||
| 781 | "category_id": 7, | ||
| 782 | "map_id": 43, | ||
| 783 | "map_name": "Propulsion Flings", | ||
| 784 | "map_wr_count": 0, | ||
| 785 | "placement": 1, | ||
| 786 | "scores": [ | ||
| 787 | { | ||
| 788 | "record_id": 319, | ||
| 789 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 790 | "score_count": 31, | ||
| 791 | "score_time": 9999, | ||
| 792 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 793 | } | ||
| 794 | ] | ||
| 795 | }, | ||
| 796 | { | ||
| 797 | "game_id": 1, | ||
| 798 | "category_id": 7, | ||
| 799 | "map_id": 44, | ||
| 800 | "map_name": "Conversion Intro", | ||
| 801 | "map_wr_count": 0, | ||
| 802 | "placement": 1, | ||
| 803 | "scores": [ | ||
| 804 | { | ||
| 805 | "record_id": 320, | ||
| 806 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 807 | "score_count": 31, | ||
| 808 | "score_time": 9999, | ||
| 809 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 810 | } | ||
| 811 | ] | ||
| 812 | }, | ||
| 813 | { | ||
| 814 | "game_id": 1, | ||
| 815 | "category_id": 7, | ||
| 816 | "map_id": 45, | ||
| 817 | "map_name": "Three Gels", | ||
| 818 | "map_wr_count": 0, | ||
| 819 | "placement": 1, | ||
| 820 | "scores": [ | ||
| 821 | { | ||
| 822 | "record_id": 321, | ||
| 823 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 824 | "score_count": 31, | ||
| 825 | "score_time": 9999, | ||
| 826 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 827 | } | ||
| 828 | ] | ||
| 829 | }, | ||
| 830 | { | ||
| 831 | "game_id": 1, | ||
| 832 | "category_id": 8, | ||
| 833 | "map_id": 46, | ||
| 834 | "map_name": "Test", | ||
| 835 | "map_wr_count": 0, | ||
| 836 | "placement": 1, | ||
| 837 | "scores": [ | ||
| 838 | { | ||
| 839 | "record_id": 322, | ||
| 840 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 841 | "score_count": 31, | ||
| 842 | "score_time": 9999, | ||
| 843 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 844 | } | ||
| 845 | ] | ||
| 846 | }, | ||
| 847 | { | ||
| 848 | "game_id": 1, | ||
| 849 | "category_id": 8, | ||
| 850 | "map_id": 47, | ||
| 851 | "map_name": "Funnel Intro", | ||
| 852 | "map_wr_count": 0, | ||
| 853 | "placement": 1, | ||
| 854 | "scores": [ | ||
| 855 | { | ||
| 856 | "record_id": 323, | ||
| 857 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 858 | "score_count": 31, | ||
| 859 | "score_time": 9999, | ||
| 860 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 861 | } | ||
| 862 | ] | ||
| 863 | }, | ||
| 864 | { | ||
| 865 | "game_id": 1, | ||
| 866 | "category_id": 8, | ||
| 867 | "map_id": 48, | ||
| 868 | "map_name": "Ceiling Button", | ||
| 869 | "map_wr_count": 0, | ||
| 870 | "placement": 1, | ||
| 871 | "scores": [ | ||
| 872 | { | ||
| 873 | "record_id": 324, | ||
| 874 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 875 | "score_count": 31, | ||
| 876 | "score_time": 9999, | ||
| 877 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 878 | } | ||
| 879 | ] | ||
| 880 | }, | ||
| 881 | { | ||
| 882 | "game_id": 1, | ||
| 883 | "category_id": 8, | ||
| 884 | "map_id": 49, | ||
| 885 | "map_name": "Wall Button", | ||
| 886 | "map_wr_count": 0, | ||
| 887 | "placement": 1, | ||
| 888 | "scores": [ | ||
| 889 | { | ||
| 890 | "record_id": 325, | ||
| 891 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 892 | "score_count": 31, | ||
| 893 | "score_time": 9999, | ||
| 894 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 895 | } | ||
| 896 | ] | ||
| 897 | }, | ||
| 898 | { | ||
| 899 | "game_id": 1, | ||
| 900 | "category_id": 8, | ||
| 901 | "map_id": 50, | ||
| 902 | "map_name": "Polarity", | ||
| 903 | "map_wr_count": 0, | ||
| 904 | "placement": 1, | ||
| 905 | "scores": [ | ||
| 906 | { | ||
| 907 | "record_id": 326, | ||
| 908 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 909 | "score_count": 31, | ||
| 910 | "score_time": 9999, | ||
| 911 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 912 | } | ||
| 913 | ] | ||
| 914 | }, | ||
| 915 | { | ||
| 916 | "game_id": 1, | ||
| 917 | "category_id": 8, | ||
| 918 | "map_id": 51, | ||
| 919 | "map_name": "Funnel Catch", | ||
| 920 | "map_wr_count": 0, | ||
| 921 | "placement": 1, | ||
| 922 | "scores": [ | ||
| 923 | { | ||
| 924 | "record_id": 327, | ||
| 925 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 926 | "score_count": 31, | ||
| 927 | "score_time": 9999, | ||
| 928 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 929 | } | ||
| 930 | ] | ||
| 931 | }, | ||
| 932 | { | ||
| 933 | "game_id": 1, | ||
| 934 | "category_id": 8, | ||
| 935 | "map_id": 52, | ||
| 936 | "map_name": "Stop The Box", | ||
| 937 | "map_wr_count": 0, | ||
| 938 | "placement": 1, | ||
| 939 | "scores": [ | ||
| 940 | { | ||
| 941 | "record_id": 328, | ||
| 942 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 943 | "score_count": 31, | ||
| 944 | "score_time": 9999, | ||
| 945 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 946 | } | ||
| 947 | ] | ||
| 948 | }, | ||
| 949 | { | ||
| 950 | "game_id": 1, | ||
| 951 | "category_id": 8, | ||
| 952 | "map_id": 53, | ||
| 953 | "map_name": "Laser Catapult", | ||
| 954 | "map_wr_count": 0, | ||
| 955 | "placement": 1, | ||
| 956 | "scores": [ | ||
| 957 | { | ||
| 958 | "record_id": 329, | ||
| 959 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 960 | "score_count": 31, | ||
| 961 | "score_time": 9999, | ||
| 962 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 963 | } | ||
| 964 | ] | ||
| 965 | }, | ||
| 966 | { | ||
| 967 | "game_id": 1, | ||
| 968 | "category_id": 8, | ||
| 969 | "map_id": 54, | ||
| 970 | "map_name": "Laser Platform", | ||
| 971 | "map_wr_count": 0, | ||
| 972 | "placement": 1, | ||
| 973 | "scores": [ | ||
| 974 | { | ||
| 975 | "record_id": 330, | ||
| 976 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 977 | "score_count": 31, | ||
| 978 | "score_time": 9999, | ||
| 979 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 980 | } | ||
| 981 | ] | ||
| 982 | }, | ||
| 983 | { | ||
| 984 | "game_id": 1, | ||
| 985 | "category_id": 8, | ||
| 986 | "map_id": 55, | ||
| 987 | "map_name": "Propulsion Catch", | ||
| 988 | "map_wr_count": 0, | ||
| 989 | "placement": 1, | ||
| 990 | "scores": [ | ||
| 991 | { | ||
| 992 | "record_id": 331, | ||
| 993 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 994 | "score_count": 31, | ||
| 995 | "score_time": 9999, | ||
| 996 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 997 | } | ||
| 998 | ] | ||
| 999 | }, | ||
| 1000 | { | ||
| 1001 | "game_id": 1, | ||
| 1002 | "category_id": 8, | ||
| 1003 | "map_id": 56, | ||
| 1004 | "map_name": "Repulsion Polarity", | ||
| 1005 | "map_wr_count": 0, | ||
| 1006 | "placement": 1, | ||
| 1007 | "scores": [ | ||
| 1008 | { | ||
| 1009 | "record_id": 332, | ||
| 1010 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 1011 | "score_count": 31, | ||
| 1012 | "score_time": 9999, | ||
| 1013 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 1014 | } | ||
| 1015 | ] | ||
| 1016 | }, | ||
| 1017 | { | ||
| 1018 | "game_id": 1, | ||
| 1019 | "category_id": 9, | ||
| 1020 | "map_id": 57, | ||
| 1021 | "map_name": "Finale 1", | ||
| 1022 | "map_wr_count": 0, | ||
| 1023 | "placement": 1, | ||
| 1024 | "scores": [ | ||
| 1025 | { | ||
| 1026 | "record_id": 333, | ||
| 1027 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 1028 | "score_count": 31, | ||
| 1029 | "score_time": 9999, | ||
| 1030 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 1031 | } | ||
| 1032 | ] | ||
| 1033 | }, | ||
| 1034 | { | ||
| 1035 | "game_id": 1, | ||
| 1036 | "category_id": 9, | ||
| 1037 | "map_id": 58, | ||
| 1038 | "map_name": "Finale 2", | ||
| 1039 | "map_wr_count": 0, | ||
| 1040 | "placement": 1, | ||
| 1041 | "scores": [ | ||
| 1042 | { | ||
| 1043 | "record_id": 334, | ||
| 1044 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 1045 | "score_count": 31, | ||
| 1046 | "score_time": 9999, | ||
| 1047 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 1048 | } | ||
| 1049 | ] | ||
| 1050 | }, | ||
| 1051 | { | ||
| 1052 | "game_id": 1, | ||
| 1053 | "category_id": 9, | ||
| 1054 | "map_id": 59, | ||
| 1055 | "map_name": "Finale 3", | ||
| 1056 | "map_wr_count": 2, | ||
| 1057 | "placement": 1, | ||
| 1058 | "scores": [ | ||
| 1059 | { | ||
| 1060 | "record_id": 335, | ||
| 1061 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 1062 | "score_count": 31, | ||
| 1063 | "score_time": 9999, | ||
| 1064 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 1065 | } | ||
| 1066 | ] | ||
| 1067 | }, | ||
| 1068 | { | ||
| 1069 | "game_id": 1, | ||
| 1070 | "category_id": 9, | ||
| 1071 | "map_id": 60, | ||
| 1072 | "map_name": "Finale 4", | ||
| 1073 | "map_wr_count": 1, | ||
| 1074 | "placement": 1, | ||
| 1075 | "scores": [ | ||
| 1076 | { | ||
| 1077 | "record_id": 336, | ||
| 1078 | "demo_id": "27b3a03e-56a3-4df3-b9bf-448fc0cbf1e7", | ||
| 1079 | "score_count": 31, | ||
| 1080 | "score_time": 9999, | ||
| 1081 | "date": "2023-09-03T19:09:11.602056Z" | ||
| 1082 | } | ||
| 1083 | ] | ||
| 1084 | }, | ||
| 1085 | { | ||
| 1086 | "game_id": 2, | ||
| 1087 | "category_id": 11, | ||
| 1088 | "map_id": 63, | ||
| 1089 | "map_name": "Doors", | ||
| 1090 | "map_wr_count": 0, | ||
| 1091 | "placement": 1, | ||
| 1092 | "scores": [ | ||
| 1093 | { | ||
| 1094 | "record_id": 5, | ||
| 1095 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1096 | "score_count": 31, | ||
| 1097 | "score_time": 9999, | ||
| 1098 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1099 | } | ||
| 1100 | ] | ||
| 1101 | }, | ||
| 1102 | { | ||
| 1103 | "game_id": 2, | ||
| 1104 | "category_id": 11, | ||
| 1105 | "map_id": 64, | ||
| 1106 | "map_name": "Buttons", | ||
| 1107 | "map_wr_count": 2, | ||
| 1108 | "placement": 1, | ||
| 1109 | "scores": [ | ||
| 1110 | { | ||
| 1111 | "record_id": 6, | ||
| 1112 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1113 | "score_count": 31, | ||
| 1114 | "score_time": 9999, | ||
| 1115 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1116 | } | ||
| 1117 | ] | ||
| 1118 | }, | ||
| 1119 | { | ||
| 1120 | "game_id": 2, | ||
| 1121 | "category_id": 11, | ||
| 1122 | "map_id": 65, | ||
| 1123 | "map_name": "Lasers", | ||
| 1124 | "map_wr_count": 2, | ||
| 1125 | "placement": 1, | ||
| 1126 | "scores": [ | ||
| 1127 | { | ||
| 1128 | "record_id": 7, | ||
| 1129 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1130 | "score_count": 31, | ||
| 1131 | "score_time": 9999, | ||
| 1132 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1133 | } | ||
| 1134 | ] | ||
| 1135 | }, | ||
| 1136 | { | ||
| 1137 | "game_id": 2, | ||
| 1138 | "category_id": 11, | ||
| 1139 | "map_id": 66, | ||
| 1140 | "map_name": "Rat Maze", | ||
| 1141 | "map_wr_count": 0, | ||
| 1142 | "placement": 1, | ||
| 1143 | "scores": [ | ||
| 1144 | { | ||
| 1145 | "record_id": 8, | ||
| 1146 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1147 | "score_count": 31, | ||
| 1148 | "score_time": 9999, | ||
| 1149 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1150 | } | ||
| 1151 | ] | ||
| 1152 | }, | ||
| 1153 | { | ||
| 1154 | "game_id": 2, | ||
| 1155 | "category_id": 11, | ||
| 1156 | "map_id": 67, | ||
| 1157 | "map_name": "Laser Crusher", | ||
| 1158 | "map_wr_count": 0, | ||
| 1159 | "placement": 1, | ||
| 1160 | "scores": [ | ||
| 1161 | { | ||
| 1162 | "record_id": 9, | ||
| 1163 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1164 | "score_count": 31, | ||
| 1165 | "score_time": 9999, | ||
| 1166 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1167 | } | ||
| 1168 | ] | ||
| 1169 | }, | ||
| 1170 | { | ||
| 1171 | "game_id": 2, | ||
| 1172 | "category_id": 11, | ||
| 1173 | "map_id": 68, | ||
| 1174 | "map_name": "Behind The Scenes", | ||
| 1175 | "map_wr_count": 0, | ||
| 1176 | "placement": 1, | ||
| 1177 | "scores": [ | ||
| 1178 | { | ||
| 1179 | "record_id": 10, | ||
| 1180 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1181 | "score_count": 31, | ||
| 1182 | "score_time": 9999, | ||
| 1183 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1184 | } | ||
| 1185 | ] | ||
| 1186 | }, | ||
| 1187 | { | ||
| 1188 | "game_id": 2, | ||
| 1189 | "category_id": 12, | ||
| 1190 | "map_id": 69, | ||
| 1191 | "map_name": "Flings", | ||
| 1192 | "map_wr_count": 4, | ||
| 1193 | "placement": 1, | ||
| 1194 | "scores": [ | ||
| 1195 | { | ||
| 1196 | "record_id": 11, | ||
| 1197 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1198 | "score_count": 31, | ||
| 1199 | "score_time": 9999, | ||
| 1200 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1201 | } | ||
| 1202 | ] | ||
| 1203 | }, | ||
| 1204 | { | ||
| 1205 | "game_id": 2, | ||
| 1206 | "category_id": 12, | ||
| 1207 | "map_id": 70, | ||
| 1208 | "map_name": "Infinifling", | ||
| 1209 | "map_wr_count": 0, | ||
| 1210 | "placement": 1, | ||
| 1211 | "scores": [ | ||
| 1212 | { | ||
| 1213 | "record_id": 12, | ||
| 1214 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1215 | "score_count": 31, | ||
| 1216 | "score_time": 9999, | ||
| 1217 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1218 | } | ||
| 1219 | ] | ||
| 1220 | }, | ||
| 1221 | { | ||
| 1222 | "game_id": 2, | ||
| 1223 | "category_id": 12, | ||
| 1224 | "map_id": 71, | ||
| 1225 | "map_name": "Team Retrieval", | ||
| 1226 | "map_wr_count": 0, | ||
| 1227 | "placement": 1, | ||
| 1228 | "scores": [ | ||
| 1229 | { | ||
| 1230 | "record_id": 13, | ||
| 1231 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1232 | "score_count": 31, | ||
| 1233 | "score_time": 9999, | ||
| 1234 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1235 | } | ||
| 1236 | ] | ||
| 1237 | }, | ||
| 1238 | { | ||
| 1239 | "game_id": 2, | ||
| 1240 | "category_id": 12, | ||
| 1241 | "map_id": 72, | ||
| 1242 | "map_name": "Vertical Flings", | ||
| 1243 | "map_wr_count": 2, | ||
| 1244 | "placement": 1, | ||
| 1245 | "scores": [ | ||
| 1246 | { | ||
| 1247 | "record_id": 14, | ||
| 1248 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1249 | "score_count": 31, | ||
| 1250 | "score_time": 9999, | ||
| 1251 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1252 | } | ||
| 1253 | ] | ||
| 1254 | }, | ||
| 1255 | { | ||
| 1256 | "game_id": 2, | ||
| 1257 | "category_id": 12, | ||
| 1258 | "map_id": 73, | ||
| 1259 | "map_name": "Catapults", | ||
| 1260 | "map_wr_count": 4, | ||
| 1261 | "placement": 1, | ||
| 1262 | "scores": [ | ||
| 1263 | { | ||
| 1264 | "record_id": 15, | ||
| 1265 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1266 | "score_count": 31, | ||
| 1267 | "score_time": 9999, | ||
| 1268 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1269 | } | ||
| 1270 | ] | ||
| 1271 | }, | ||
| 1272 | { | ||
| 1273 | "game_id": 2, | ||
| 1274 | "category_id": 12, | ||
| 1275 | "map_id": 74, | ||
| 1276 | "map_name": "Multifling", | ||
| 1277 | "map_wr_count": 2, | ||
| 1278 | "placement": 1, | ||
| 1279 | "scores": [ | ||
| 1280 | { | ||
| 1281 | "record_id": 16, | ||
| 1282 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1283 | "score_count": 31, | ||
| 1284 | "score_time": 9999, | ||
| 1285 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1286 | } | ||
| 1287 | ] | ||
| 1288 | }, | ||
| 1289 | { | ||
| 1290 | "game_id": 2, | ||
| 1291 | "category_id": 12, | ||
| 1292 | "map_id": 75, | ||
| 1293 | "map_name": "Fling Crushers", | ||
| 1294 | "map_wr_count": 0, | ||
| 1295 | "placement": 1, | ||
| 1296 | "scores": [ | ||
| 1297 | { | ||
| 1298 | "record_id": 17, | ||
| 1299 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1300 | "score_count": 31, | ||
| 1301 | "score_time": 9999, | ||
| 1302 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1303 | } | ||
| 1304 | ] | ||
| 1305 | }, | ||
| 1306 | { | ||
| 1307 | "game_id": 2, | ||
| 1308 | "category_id": 12, | ||
| 1309 | "map_id": 76, | ||
| 1310 | "map_name": "Industrial Fan", | ||
| 1311 | "map_wr_count": 0, | ||
| 1312 | "placement": 1, | ||
| 1313 | "scores": [ | ||
| 1314 | { | ||
| 1315 | "record_id": 18, | ||
| 1316 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1317 | "score_count": 31, | ||
| 1318 | "score_time": 9999, | ||
| 1319 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1320 | } | ||
| 1321 | ] | ||
| 1322 | }, | ||
| 1323 | { | ||
| 1324 | "game_id": 2, | ||
| 1325 | "category_id": 13, | ||
| 1326 | "map_id": 77, | ||
| 1327 | "map_name": "Cooperative Bridges", | ||
| 1328 | "map_wr_count": 3, | ||
| 1329 | "placement": 1, | ||
| 1330 | "scores": [ | ||
| 1331 | { | ||
| 1332 | "record_id": 19, | ||
| 1333 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1334 | "score_count": 31, | ||
| 1335 | "score_time": 9999, | ||
| 1336 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1337 | } | ||
| 1338 | ] | ||
| 1339 | }, | ||
| 1340 | { | ||
| 1341 | "game_id": 2, | ||
| 1342 | "category_id": 13, | ||
| 1343 | "map_id": 78, | ||
| 1344 | "map_name": "Bridge Swap", | ||
| 1345 | "map_wr_count": 2, | ||
| 1346 | "placement": 1, | ||
| 1347 | "scores": [ | ||
| 1348 | { | ||
| 1349 | "record_id": 20, | ||
| 1350 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1351 | "score_count": 31, | ||
| 1352 | "score_time": 9999, | ||
| 1353 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1354 | } | ||
| 1355 | ] | ||
| 1356 | }, | ||
| 1357 | { | ||
| 1358 | "game_id": 2, | ||
| 1359 | "category_id": 13, | ||
| 1360 | "map_id": 79, | ||
| 1361 | "map_name": "Fling Block", | ||
| 1362 | "map_wr_count": 0, | ||
| 1363 | "placement": 1, | ||
| 1364 | "scores": [ | ||
| 1365 | { | ||
| 1366 | "record_id": 4, | ||
| 1367 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1368 | "score_count": 0, | ||
| 1369 | "score_time": 43368, | ||
| 1370 | "date": "2023-08-30T13:16:56.91335Z" | ||
| 1371 | }, | ||
| 1372 | { | ||
| 1373 | "record_id": 21, | ||
| 1374 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1375 | "score_count": 31, | ||
| 1376 | "score_time": 9999, | ||
| 1377 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1378 | } | ||
| 1379 | ] | ||
| 1380 | }, | ||
| 1381 | { | ||
| 1382 | "game_id": 2, | ||
| 1383 | "category_id": 13, | ||
| 1384 | "map_id": 80, | ||
| 1385 | "map_name": "Catapult Block", | ||
| 1386 | "map_wr_count": 4, | ||
| 1387 | "placement": 2, | ||
| 1388 | "scores": [ | ||
| 1389 | { | ||
| 1390 | "record_id": 22, | ||
| 1391 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1392 | "score_count": 31, | ||
| 1393 | "score_time": 9999, | ||
| 1394 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1395 | } | ||
| 1396 | ] | ||
| 1397 | }, | ||
| 1398 | { | ||
| 1399 | "game_id": 2, | ||
| 1400 | "category_id": 13, | ||
| 1401 | "map_id": 81, | ||
| 1402 | "map_name": "Bridge Fling", | ||
| 1403 | "map_wr_count": 2, | ||
| 1404 | "placement": 1, | ||
| 1405 | "scores": [ | ||
| 1406 | { | ||
| 1407 | "record_id": 23, | ||
| 1408 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1409 | "score_count": 31, | ||
| 1410 | "score_time": 9999, | ||
| 1411 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1412 | } | ||
| 1413 | ] | ||
| 1414 | }, | ||
| 1415 | { | ||
| 1416 | "game_id": 2, | ||
| 1417 | "category_id": 13, | ||
| 1418 | "map_id": 82, | ||
| 1419 | "map_name": "Turret Walls", | ||
| 1420 | "map_wr_count": 4, | ||
| 1421 | "placement": 1, | ||
| 1422 | "scores": [ | ||
| 1423 | { | ||
| 1424 | "record_id": 24, | ||
| 1425 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1426 | "score_count": 31, | ||
| 1427 | "score_time": 9999, | ||
| 1428 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1429 | } | ||
| 1430 | ] | ||
| 1431 | }, | ||
| 1432 | { | ||
| 1433 | "game_id": 2, | ||
| 1434 | "category_id": 13, | ||
| 1435 | "map_id": 83, | ||
| 1436 | "map_name": "Turret Assasin", | ||
| 1437 | "map_wr_count": 0, | ||
| 1438 | "placement": 1, | ||
| 1439 | "scores": [ | ||
| 1440 | { | ||
| 1441 | "record_id": 25, | ||
| 1442 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1443 | "score_count": 31, | ||
| 1444 | "score_time": 9999, | ||
| 1445 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1446 | } | ||
| 1447 | ] | ||
| 1448 | }, | ||
| 1449 | { | ||
| 1450 | "game_id": 2, | ||
| 1451 | "category_id": 13, | ||
| 1452 | "map_id": 84, | ||
| 1453 | "map_name": "Bridge Testing", | ||
| 1454 | "map_wr_count": 0, | ||
| 1455 | "placement": 1, | ||
| 1456 | "scores": [ | ||
| 1457 | { | ||
| 1458 | "record_id": 26, | ||
| 1459 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1460 | "score_count": 31, | ||
| 1461 | "score_time": 9999, | ||
| 1462 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1463 | } | ||
| 1464 | ] | ||
| 1465 | }, | ||
| 1466 | { | ||
| 1467 | "game_id": 2, | ||
| 1468 | "category_id": 14, | ||
| 1469 | "map_id": 85, | ||
| 1470 | "map_name": "Cooperative Funnels", | ||
| 1471 | "map_wr_count": 0, | ||
| 1472 | "placement": 1, | ||
| 1473 | "scores": [ | ||
| 1474 | { | ||
| 1475 | "record_id": 27, | ||
| 1476 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1477 | "score_count": 31, | ||
| 1478 | "score_time": 9999, | ||
| 1479 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1480 | } | ||
| 1481 | ] | ||
| 1482 | }, | ||
| 1483 | { | ||
| 1484 | "game_id": 2, | ||
| 1485 | "category_id": 14, | ||
| 1486 | "map_id": 86, | ||
| 1487 | "map_name": "Funnel Drill", | ||
| 1488 | "map_wr_count": 0, | ||
| 1489 | "placement": 1, | ||
| 1490 | "scores": [ | ||
| 1491 | { | ||
| 1492 | "record_id": 28, | ||
| 1493 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1494 | "score_count": 31, | ||
| 1495 | "score_time": 9999, | ||
| 1496 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1497 | } | ||
| 1498 | ] | ||
| 1499 | }, | ||
| 1500 | { | ||
| 1501 | "game_id": 2, | ||
| 1502 | "category_id": 14, | ||
| 1503 | "map_id": 87, | ||
| 1504 | "map_name": "Funnel Catch", | ||
| 1505 | "map_wr_count": 0, | ||
| 1506 | "placement": 1, | ||
| 1507 | "scores": [ | ||
| 1508 | { | ||
| 1509 | "record_id": 29, | ||
| 1510 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1511 | "score_count": 31, | ||
| 1512 | "score_time": 9999, | ||
| 1513 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1514 | } | ||
| 1515 | ] | ||
| 1516 | }, | ||
| 1517 | { | ||
| 1518 | "game_id": 2, | ||
| 1519 | "category_id": 14, | ||
| 1520 | "map_id": 88, | ||
| 1521 | "map_name": "Funnel Laser", | ||
| 1522 | "map_wr_count": 0, | ||
| 1523 | "placement": 1, | ||
| 1524 | "scores": [ | ||
| 1525 | { | ||
| 1526 | "record_id": 30, | ||
| 1527 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1528 | "score_count": 31, | ||
| 1529 | "score_time": 9999, | ||
| 1530 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1531 | } | ||
| 1532 | ] | ||
| 1533 | }, | ||
| 1534 | { | ||
| 1535 | "game_id": 2, | ||
| 1536 | "category_id": 14, | ||
| 1537 | "map_id": 89, | ||
| 1538 | "map_name": "Cooperative Polarity", | ||
| 1539 | "map_wr_count": 0, | ||
| 1540 | "placement": 1, | ||
| 1541 | "scores": [ | ||
| 1542 | { | ||
| 1543 | "record_id": 31, | ||
| 1544 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1545 | "score_count": 31, | ||
| 1546 | "score_time": 9999, | ||
| 1547 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1548 | } | ||
| 1549 | ] | ||
| 1550 | }, | ||
| 1551 | { | ||
| 1552 | "game_id": 2, | ||
| 1553 | "category_id": 14, | ||
| 1554 | "map_id": 90, | ||
| 1555 | "map_name": "Funnel Hop", | ||
| 1556 | "map_wr_count": 0, | ||
| 1557 | "placement": 1, | ||
| 1558 | "scores": [ | ||
| 1559 | { | ||
| 1560 | "record_id": 32, | ||
| 1561 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1562 | "score_count": 31, | ||
| 1563 | "score_time": 9999, | ||
| 1564 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1565 | } | ||
| 1566 | ] | ||
| 1567 | }, | ||
| 1568 | { | ||
| 1569 | "game_id": 2, | ||
| 1570 | "category_id": 14, | ||
| 1571 | "map_id": 91, | ||
| 1572 | "map_name": "Advanced Polarity", | ||
| 1573 | "map_wr_count": 0, | ||
| 1574 | "placement": 1, | ||
| 1575 | "scores": [ | ||
| 1576 | { | ||
| 1577 | "record_id": 33, | ||
| 1578 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1579 | "score_count": 31, | ||
| 1580 | "score_time": 9999, | ||
| 1581 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1582 | } | ||
| 1583 | ] | ||
| 1584 | }, | ||
| 1585 | { | ||
| 1586 | "game_id": 2, | ||
| 1587 | "category_id": 14, | ||
| 1588 | "map_id": 92, | ||
| 1589 | "map_name": "Funnel Maze", | ||
| 1590 | "map_wr_count": 0, | ||
| 1591 | "placement": 1, | ||
| 1592 | "scores": [ | ||
| 1593 | { | ||
| 1594 | "record_id": 34, | ||
| 1595 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1596 | "score_count": 31, | ||
| 1597 | "score_time": 9999, | ||
| 1598 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1599 | } | ||
| 1600 | ] | ||
| 1601 | }, | ||
| 1602 | { | ||
| 1603 | "game_id": 2, | ||
| 1604 | "category_id": 14, | ||
| 1605 | "map_id": 93, | ||
| 1606 | "map_name": "Turret Warehouse", | ||
| 1607 | "map_wr_count": 0, | ||
| 1608 | "placement": 1, | ||
| 1609 | "scores": [ | ||
| 1610 | { | ||
| 1611 | "record_id": 35, | ||
| 1612 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1613 | "score_count": 31, | ||
| 1614 | "score_time": 9999, | ||
| 1615 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1616 | } | ||
| 1617 | ] | ||
| 1618 | }, | ||
| 1619 | { | ||
| 1620 | "game_id": 2, | ||
| 1621 | "category_id": 15, | ||
| 1622 | "map_id": 94, | ||
| 1623 | "map_name": "Repulsion Jumps", | ||
| 1624 | "map_wr_count": 0, | ||
| 1625 | "placement": 1, | ||
| 1626 | "scores": [ | ||
| 1627 | { | ||
| 1628 | "record_id": 36, | ||
| 1629 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1630 | "score_count": 31, | ||
| 1631 | "score_time": 9999, | ||
| 1632 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1633 | } | ||
| 1634 | ] | ||
| 1635 | }, | ||
| 1636 | { | ||
| 1637 | "game_id": 2, | ||
| 1638 | "category_id": 15, | ||
| 1639 | "map_id": 95, | ||
| 1640 | "map_name": "Double Bounce", | ||
| 1641 | "map_wr_count": 0, | ||
| 1642 | "placement": 1, | ||
| 1643 | "scores": [ | ||
| 1644 | { | ||
| 1645 | "record_id": 37, | ||
| 1646 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1647 | "score_count": 31, | ||
| 1648 | "score_time": 9999, | ||
| 1649 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1650 | } | ||
| 1651 | ] | ||
| 1652 | }, | ||
| 1653 | { | ||
| 1654 | "game_id": 2, | ||
| 1655 | "category_id": 15, | ||
| 1656 | "map_id": 96, | ||
| 1657 | "map_name": "Bridge Repulsion", | ||
| 1658 | "map_wr_count": 2, | ||
| 1659 | "placement": 1, | ||
| 1660 | "scores": [ | ||
| 1661 | { | ||
| 1662 | "record_id": 38, | ||
| 1663 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1664 | "score_count": 31, | ||
| 1665 | "score_time": 9999, | ||
| 1666 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1667 | } | ||
| 1668 | ] | ||
| 1669 | }, | ||
| 1670 | { | ||
| 1671 | "game_id": 2, | ||
| 1672 | "category_id": 15, | ||
| 1673 | "map_id": 97, | ||
| 1674 | "map_name": "Wall Repulsion", | ||
| 1675 | "map_wr_count": 2, | ||
| 1676 | "placement": 1, | ||
| 1677 | "scores": [ | ||
| 1678 | { | ||
| 1679 | "record_id": 39, | ||
| 1680 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1681 | "score_count": 31, | ||
| 1682 | "score_time": 9999, | ||
| 1683 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1684 | } | ||
| 1685 | ] | ||
| 1686 | }, | ||
| 1687 | { | ||
| 1688 | "game_id": 2, | ||
| 1689 | "category_id": 15, | ||
| 1690 | "map_id": 98, | ||
| 1691 | "map_name": "Propulsion Crushers", | ||
| 1692 | "map_wr_count": 0, | ||
| 1693 | "placement": 1, | ||
| 1694 | "scores": [ | ||
| 1695 | { | ||
| 1696 | "record_id": 40, | ||
| 1697 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1698 | "score_count": 31, | ||
| 1699 | "score_time": 9999, | ||
| 1700 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1701 | } | ||
| 1702 | ] | ||
| 1703 | }, | ||
| 1704 | { | ||
| 1705 | "game_id": 2, | ||
| 1706 | "category_id": 15, | ||
| 1707 | "map_id": 99, | ||
| 1708 | "map_name": "Turret Ninja", | ||
| 1709 | "map_wr_count": 0, | ||
| 1710 | "placement": 1, | ||
| 1711 | "scores": [ | ||
| 1712 | { | ||
| 1713 | "record_id": 41, | ||
| 1714 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1715 | "score_count": 31, | ||
| 1716 | "score_time": 9999, | ||
| 1717 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1718 | } | ||
| 1719 | ] | ||
| 1720 | }, | ||
| 1721 | { | ||
| 1722 | "game_id": 2, | ||
| 1723 | "category_id": 15, | ||
| 1724 | "map_id": 100, | ||
| 1725 | "map_name": "Propulsion Retrieval", | ||
| 1726 | "map_wr_count": 0, | ||
| 1727 | "placement": 1, | ||
| 1728 | "scores": [ | ||
| 1729 | { | ||
| 1730 | "record_id": 42, | ||
| 1731 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1732 | "score_count": 31, | ||
| 1733 | "score_time": 9999, | ||
| 1734 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1735 | } | ||
| 1736 | ] | ||
| 1737 | }, | ||
| 1738 | { | ||
| 1739 | "game_id": 2, | ||
| 1740 | "category_id": 15, | ||
| 1741 | "map_id": 101, | ||
| 1742 | "map_name": "Vault Entrance", | ||
| 1743 | "map_wr_count": 0, | ||
| 1744 | "placement": 1, | ||
| 1745 | "scores": [ | ||
| 1746 | { | ||
| 1747 | "record_id": 43, | ||
| 1748 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1749 | "score_count": 31, | ||
| 1750 | "score_time": 9999, | ||
| 1751 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1752 | } | ||
| 1753 | ] | ||
| 1754 | }, | ||
| 1755 | { | ||
| 1756 | "game_id": 2, | ||
| 1757 | "category_id": 16, | ||
| 1758 | "map_id": 102, | ||
| 1759 | "map_name": "Separation", | ||
| 1760 | "map_wr_count": 0, | ||
| 1761 | "placement": 1, | ||
| 1762 | "scores": [ | ||
| 1763 | { | ||
| 1764 | "record_id": 44, | ||
| 1765 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1766 | "score_count": 31, | ||
| 1767 | "score_time": 9999, | ||
| 1768 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1769 | } | ||
| 1770 | ] | ||
| 1771 | }, | ||
| 1772 | { | ||
| 1773 | "game_id": 2, | ||
| 1774 | "category_id": 16, | ||
| 1775 | "map_id": 103, | ||
| 1776 | "map_name": "Triple Axis", | ||
| 1777 | "map_wr_count": 0, | ||
| 1778 | "placement": 1, | ||
| 1779 | "scores": [ | ||
| 1780 | { | ||
| 1781 | "record_id": 45, | ||
| 1782 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1783 | "score_count": 31, | ||
| 1784 | "score_time": 9999, | ||
| 1785 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1786 | } | ||
| 1787 | ] | ||
| 1788 | }, | ||
| 1789 | { | ||
| 1790 | "game_id": 2, | ||
| 1791 | "category_id": 16, | ||
| 1792 | "map_id": 104, | ||
| 1793 | "map_name": "Catapult Catch", | ||
| 1794 | "map_wr_count": 0, | ||
| 1795 | "placement": 1, | ||
| 1796 | "scores": [ | ||
| 1797 | { | ||
| 1798 | "record_id": 46, | ||
| 1799 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1800 | "score_count": 31, | ||
| 1801 | "score_time": 9999, | ||
| 1802 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1803 | } | ||
| 1804 | ] | ||
| 1805 | }, | ||
| 1806 | { | ||
| 1807 | "game_id": 2, | ||
| 1808 | "category_id": 16, | ||
| 1809 | "map_id": 105, | ||
| 1810 | "map_name": "Bridge Gels", | ||
| 1811 | "map_wr_count": 2, | ||
| 1812 | "placement": 1, | ||
| 1813 | "scores": [ | ||
| 1814 | { | ||
| 1815 | "record_id": 47, | ||
| 1816 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1817 | "score_count": 31, | ||
| 1818 | "score_time": 9999, | ||
| 1819 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1820 | } | ||
| 1821 | ] | ||
| 1822 | }, | ||
| 1823 | { | ||
| 1824 | "game_id": 2, | ||
| 1825 | "category_id": 16, | ||
| 1826 | "map_id": 106, | ||
| 1827 | "map_name": "Maintenance", | ||
| 1828 | "map_wr_count": 0, | ||
| 1829 | "placement": 1, | ||
| 1830 | "scores": [ | ||
| 1831 | { | ||
| 1832 | "record_id": 48, | ||
| 1833 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1834 | "score_count": 31, | ||
| 1835 | "score_time": 9999, | ||
| 1836 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1837 | } | ||
| 1838 | ] | ||
| 1839 | }, | ||
| 1840 | { | ||
| 1841 | "game_id": 2, | ||
| 1842 | "category_id": 16, | ||
| 1843 | "map_id": 107, | ||
| 1844 | "map_name": "Bridge Catch", | ||
| 1845 | "map_wr_count": 0, | ||
| 1846 | "placement": 1, | ||
| 1847 | "scores": [ | ||
| 1848 | { | ||
| 1849 | "record_id": 49, | ||
| 1850 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1851 | "score_count": 31, | ||
| 1852 | "score_time": 9999, | ||
| 1853 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1854 | } | ||
| 1855 | ] | ||
| 1856 | }, | ||
| 1857 | { | ||
| 1858 | "game_id": 2, | ||
| 1859 | "category_id": 16, | ||
| 1860 | "map_id": 108, | ||
| 1861 | "map_name": "Double Lift", | ||
| 1862 | "map_wr_count": 0, | ||
| 1863 | "placement": 1, | ||
| 1864 | "scores": [ | ||
| 1865 | { | ||
| 1866 | "record_id": 50, | ||
| 1867 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1868 | "score_count": 31, | ||
| 1869 | "score_time": 9999, | ||
| 1870 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1871 | } | ||
| 1872 | ] | ||
| 1873 | }, | ||
| 1874 | { | ||
| 1875 | "game_id": 2, | ||
| 1876 | "category_id": 16, | ||
| 1877 | "map_id": 109, | ||
| 1878 | "map_name": "Gel Maze", | ||
| 1879 | "map_wr_count": 0, | ||
| 1880 | "placement": 1, | ||
| 1881 | "scores": [ | ||
| 1882 | { | ||
| 1883 | "record_id": 51, | ||
| 1884 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1885 | "score_count": 31, | ||
| 1886 | "score_time": 9999, | ||
| 1887 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1888 | } | ||
| 1889 | ] | ||
| 1890 | }, | ||
| 1891 | { | ||
| 1892 | "game_id": 2, | ||
| 1893 | "category_id": 16, | ||
| 1894 | "map_id": 110, | ||
| 1895 | "map_name": "Crazier Box", | ||
| 1896 | "map_wr_count": 0, | ||
| 1897 | "placement": 1, | ||
| 1898 | "scores": [ | ||
| 1899 | { | ||
| 1900 | "record_id": 52, | ||
| 1901 | "demo_id": "b8d6adc2-d8b7-41e4-8faf-63f246d910cd", | ||
| 1902 | "score_count": 31, | ||
| 1903 | "score_time": 9999, | ||
| 1904 | "date": "2023-09-03T19:12:05.958456Z" | ||
| 1905 | } | ||
| 1906 | ] | ||
| 1907 | } | ||
| 1908 | ], | ||
| 1909 | "pagination": { | ||
| 1910 | "total_records": 0, | ||
| 1911 | "total_pages": 0, | ||
| 1912 | "current_page": 0, | ||
| 1913 | "page_size": 0 | ||
| 1914 | } | ||
| 1915 | |||
| 1916 | } | ||
| 1917 | |||
| 1918 | ) | ||
| 1919 | }}> | ||
| 1920 | <img src={UserIcon} alt="" /> | ||
| 1921 | <span> | ||
| 1922 | <img src={LoginIcon} alt="Sign in through Steam" /> | ||
| 1923 | </span> | ||
| 1924 | </button> | ||
| 1925 | </Link> | ||
| 1926 | )} | ||
| 1927 | </> | ||
| 1928 | ); | ||
| 1929 | }; | ||
| 1930 | |||
| 1931 | export default Login; | ||
diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx new file mode 100644 index 0000000..1fe4239 --- /dev/null +++ b/frontend/src/components/ModMenu.tsx | |||
| @@ -0,0 +1,324 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import ReactMarkdown from 'react-markdown'; | ||
| 3 | |||
| 4 | import { MapSummary } from '../types/Map'; | ||
| 5 | import { ModMenuContent } from '../types/Content'; | ||
| 6 | import { API } from '../api/Api'; | ||
| 7 | import "../css/ModMenu.css" | ||
| 8 | |||
| 9 | interface ModMenuProps { | ||
| 10 | data: MapSummary; | ||
| 11 | selectedRun: number; | ||
| 12 | mapID: string; | ||
| 13 | } | ||
| 14 | |||
| 15 | const ModMenu: React.FC<ModMenuProps> = ({ data, selectedRun, mapID }) => { | ||
| 16 | |||
| 17 | const [menu, setMenu] = React.useState<number>(0); | ||
| 18 | const [showButton, setShowButton] = React.useState(1) | ||
| 19 | |||
| 20 | const [routeContent, setRouteContent] = React.useState<ModMenuContent>({ | ||
| 21 | id: 0, | ||
| 22 | name: "", | ||
| 23 | score: 0, | ||
| 24 | date: "", | ||
| 25 | showcase: "", | ||
| 26 | description: "No description available.", | ||
| 27 | category_id: 1, | ||
| 28 | }); | ||
| 29 | |||
| 30 | const [image, setImage] = React.useState<string>(""); | ||
| 31 | const [md, setMd] = React.useState<string>(""); | ||
| 32 | |||
| 33 | function compressImage(file: File): Promise<string> { | ||
| 34 | const reader = new FileReader(); | ||
| 35 | reader.readAsDataURL(file); | ||
| 36 | return new Promise(resolve => { | ||
| 37 | reader.onload = () => { | ||
| 38 | const img = new Image(); | ||
| 39 | if (typeof reader.result === "string") { | ||
| 40 | img.src = reader.result; | ||
| 41 | img.onload = () => { | ||
| 42 | let { width, height } = img; | ||
| 43 | if (width > 550) { | ||
| 44 | height *= 550 / width; | ||
| 45 | width = 550; | ||
| 46 | } | ||
| 47 | if (height > 320) { | ||
| 48 | width *= 320 / height; | ||
| 49 | height = 320; | ||
| 50 | } | ||
| 51 | const canvas = document.createElement('canvas'); | ||
| 52 | canvas.width = width; | ||
| 53 | canvas.height = height; | ||
| 54 | canvas.getContext('2d')!.drawImage(img, 0, 0, width, height); | ||
| 55 | resolve(canvas.toDataURL(file.type, 0.6)); | ||
| 56 | }; | ||
| 57 | } | ||
| 58 | }; | ||
| 59 | }); | ||
| 60 | }; | ||
| 61 | |||
| 62 | const _edit_map_summary_image = async () => { | ||
| 63 | if (window.confirm("Are you sure you want to submit this to the database?")) { | ||
| 64 | await API.put_map_image(mapID, image); | ||
| 65 | } | ||
| 66 | }; | ||
| 67 | |||
| 68 | const _edit_map_summary_route = async () => { | ||
| 69 | if (window.confirm("Are you sure you want to submit this to the database?")) { | ||
| 70 | await API.put_map_summary(mapID, routeContent); | ||
| 71 | } | ||
| 72 | }; | ||
| 73 | |||
| 74 | const _create_map_summary_route = async () => { | ||
| 75 | if (window.confirm("Are you sure you want to submit this to the database?")) { | ||
| 76 | await API.post_map_summary(mapID, routeContent); | ||
| 77 | } | ||
| 78 | }; | ||
| 79 | |||
| 80 | const _delete_map_summary_route = async () => { | ||
| 81 | if (window.confirm(`Are you sure you want to delete this run from the database? | ||
| 82 | ${data.summary.routes[selectedRun].category.name} ${data.summary.routes[selectedRun].history.score_count} portals ${data.summary.routes[selectedRun].history.runner_name}`)) { | ||
| 83 | await API.delete_map_summary(mapID, data.summary.routes[selectedRun].route_id); | ||
| 84 | } | ||
| 85 | }; | ||
| 86 | |||
| 87 | React.useEffect(() => { | ||
| 88 | if (menu === 3) { // add route | ||
| 89 | setRouteContent({ | ||
| 90 | id: 0, | ||
| 91 | name: "", | ||
| 92 | score: 0, | ||
| 93 | date: "", | ||
| 94 | showcase: "", | ||
| 95 | description: "No description available.", | ||
| 96 | category_id: 1, | ||
| 97 | }); | ||
| 98 | setMd("No description available."); | ||
| 99 | } | ||
| 100 | if (menu === 2) { // edit route | ||
| 101 | setRouteContent({ | ||
| 102 | id: data.summary.routes[selectedRun].route_id, | ||
| 103 | name: data.summary.routes[selectedRun].history.runner_name, | ||
| 104 | score: data.summary.routes[selectedRun].history.score_count, | ||
| 105 | date: data.summary.routes[selectedRun].history.date.split("T")[0], | ||
| 106 | showcase: data.summary.routes[selectedRun].showcase, | ||
| 107 | description: data.summary.routes[selectedRun].description, | ||
| 108 | category_id: data.summary.routes[selectedRun].category.id, | ||
| 109 | }); | ||
| 110 | setMd(data.summary.routes[selectedRun].description); | ||
| 111 | } | ||
| 112 | }, [menu]); | ||
| 113 | |||
| 114 | React.useEffect(() => { | ||
| 115 | const modview = document.querySelector("div#modview") as HTMLElement | ||
| 116 | if (modview) { | ||
| 117 | showButton ? modview.style.transform = "translateY(-68%)" | ||
| 118 | : modview.style.transform = "translateY(0%)" | ||
| 119 | } | ||
| 120 | |||
| 121 | const modview_block = document.querySelector("#modview_block") as HTMLElement | ||
| 122 | if (modview_block) { | ||
| 123 | showButton === 1 ? modview_block.style.display = "none" : modview_block.style.display = "block"// eslint-disable-next-line | ||
| 124 | } | ||
| 125 | }, [showButton]) | ||
| 126 | |||
| 127 | return ( | ||
| 128 | <div id="modview_bdlock"> | ||
| 129 | |||
| 130 | <div id='modview'> | ||
| 131 | <div> | ||
| 132 | <button onClick={() => setMenu(1)}>Edit Image</button> | ||
| 133 | <button onClick={() => setMenu(2)}>Edit Selected Route</button> | ||
| 134 | <button onClick={() => setMenu(3)}>Add New Route</button> | ||
| 135 | <button onClick={() => _delete_map_summary_route()}>Delete Selected Route</button> | ||
| 136 | </div> | ||
| 137 | <div> | ||
| 138 | {showButton ? ( | ||
| 139 | <button onClick={() => setShowButton(0)}>Show</button> | ||
| 140 | ) : ( | ||
| 141 | <button onClick={() => { setShowButton(1); setMenu(0) }}>Hide</button> | ||
| 142 | )} | ||
| 143 | </div> | ||
| 144 | </div> | ||
| 145 | |||
| 146 | <div id='modview-menu'> | ||
| 147 | { // Edit Image | ||
| 148 | menu === 1 && ( | ||
| 149 | <div id='modview-menu-image'> | ||
| 150 | <div> | ||
| 151 | <span>Current Image:</span> | ||
| 152 | <img src={data.map.image} alt="missing" /> | ||
| 153 | </div> | ||
| 154 | |||
| 155 | <div> | ||
| 156 | <span>New Image: | ||
| 157 | <input type="file" accept='image/*' onChange={e => { | ||
| 158 | if (e.target.files) { | ||
| 159 | compressImage(e.target.files[0]) | ||
| 160 | .then(d => setImage(d)) | ||
| 161 | } | ||
| 162 | } | ||
| 163 | } /></span> | ||
| 164 | {image ? (<button onClick={() => _edit_map_summary_image()}>upload</button>) : <span></span>} | ||
| 165 | <img src={image} alt="" id='modview-menu-image-file' /> | ||
| 166 | |||
| 167 | </div> | ||
| 168 | </div> | ||
| 169 | ) | ||
| 170 | } | ||
| 171 | |||
| 172 | { // Edit Route | ||
| 173 | menu === 2 && ( | ||
| 174 | <div id='modview-menu-edit'> | ||
| 175 | <div id='modview-route-id'> | ||
| 176 | <span>Route ID:</span> | ||
| 177 | <input type="number" value={routeContent.id} disabled /> | ||
| 178 | </div> | ||
| 179 | <div id='modview-route-name'> | ||
| 180 | <span>Runner Name:</span> | ||
| 181 | <input type="text" value={routeContent.name} onChange={(e) => { | ||
| 182 | setRouteContent({ | ||
| 183 | ...routeContent, | ||
| 184 | name: e.target.value, | ||
| 185 | }); | ||
| 186 | }} /> | ||
| 187 | </div> | ||
| 188 | <div id='modview-route-score'> | ||
| 189 | <span>Score:</span> | ||
| 190 | <input type="number" value={routeContent.score} onChange={(e) => { | ||
| 191 | setRouteContent({ | ||
| 192 | ...routeContent, | ||
| 193 | score: parseInt(e.target.value), | ||
| 194 | }); | ||
| 195 | }} /> | ||
| 196 | </div> | ||
| 197 | <div id='modview-route-date'> | ||
| 198 | <span>Date:</span> | ||
| 199 | <input type="date" value={routeContent.date} onChange={(e) => { | ||
| 200 | setRouteContent({ | ||
| 201 | ...routeContent, | ||
| 202 | date: e.target.value, | ||
| 203 | }); | ||
| 204 | }} /> | ||
| 205 | </div> | ||
| 206 | <div id='modview-route-showcase'> | ||
| 207 | <span>Showcase Video:</span> | ||
| 208 | <input type="text" value={routeContent.showcase} onChange={(e) => { | ||
| 209 | setRouteContent({ | ||
| 210 | ...routeContent, | ||
| 211 | showcase: e.target.value, | ||
| 212 | }); | ||
| 213 | }} /> | ||
| 214 | </div> | ||
| 215 | <div id='modview-route-description' style={{ height: "180px", gridColumn: "1 / span 5" }}> | ||
| 216 | <span>Description:</span> | ||
| 217 | <textarea value={routeContent.description} onChange={(e) => { | ||
| 218 | setRouteContent({ | ||
| 219 | ...routeContent, | ||
| 220 | description: e.target.value, | ||
| 221 | }); | ||
| 222 | setMd(routeContent.description); | ||
| 223 | }} /> | ||
| 224 | </div> | ||
| 225 | <button style={{ gridColumn: "2 / span 3", height: "40px" }} onClick={_edit_map_summary_route}>Apply</button> | ||
| 226 | |||
| 227 | <div id='modview-md'> | ||
| 228 | <span>Markdown Preview</span> | ||
| 229 | <span><a href="https://commonmark.org/help/" rel="noreferrer" target='_blank'>Documentation</a></span> | ||
| 230 | <span><a href="https://remarkjs.github.io/react-markdown/" rel="noreferrer" target='_blank'>Demo</a></span> | ||
| 231 | <p> | ||
| 232 | <ReactMarkdown>{md} | ||
| 233 | </ReactMarkdown> | ||
| 234 | </p> | ||
| 235 | </div> | ||
| 236 | </div> | ||
| 237 | ) | ||
| 238 | } | ||
| 239 | |||
| 240 | { // Add Route | ||
| 241 | menu === 3 && ( | ||
| 242 | <div id='modview-menu-add'> | ||
| 243 | <div id='modview-route-category'> | ||
| 244 | <span>Category:</span> | ||
| 245 | <select onChange={(e) => { | ||
| 246 | setRouteContent({ | ||
| 247 | ...routeContent, | ||
| 248 | category_id: parseInt(e.target.value), | ||
| 249 | }); | ||
| 250 | }}> | ||
| 251 | <option value="1" key="1">CM</option> | ||
| 252 | <option value="2" key="2">No SLA</option> | ||
| 253 | {data.map.game_name === "Portal 2 - Cooperative" ? "" : ( | ||
| 254 | <option value="3" key="3">Inbounds SLA</option>)} | ||
| 255 | <option value="4" key="4">Any%</option> | ||
| 256 | </select> | ||
| 257 | </div> | ||
| 258 | <div id='modview-route-name'> | ||
| 259 | <span>Runner Name:</span> | ||
| 260 | <input type="text" value={routeContent.name} onChange={(e) => { | ||
| 261 | setRouteContent({ | ||
| 262 | ...routeContent, | ||
| 263 | name: e.target.value, | ||
| 264 | }); | ||
| 265 | }} /> | ||
| 266 | </div> | ||
| 267 | <div id='modview-route-score'> | ||
| 268 | <span>Score:</span> | ||
| 269 | <input type="number" value={routeContent.score} onChange={(e) => { | ||
| 270 | setRouteContent({ | ||
| 271 | ...routeContent, | ||
| 272 | score: parseInt(e.target.value), | ||
| 273 | }); | ||
| 274 | }} /> | ||
| 275 | </div> | ||
| 276 | <div id='modview-route-date'> | ||
| 277 | <span>Date:</span> | ||
| 278 | <input type="date" value={routeContent.date} onChange={(e) => { | ||
| 279 | setRouteContent({ | ||
| 280 | ...routeContent, | ||
| 281 | date: e.target.value, | ||
| 282 | }); | ||
| 283 | }} /> | ||
| 284 | </div> | ||
| 285 | <div id='modview-route-showcase'> | ||
| 286 | <span>Showcase Video:</span> | ||
| 287 | <input type="text" value={routeContent.showcase} onChange={(e) => { | ||
| 288 | setRouteContent({ | ||
| 289 | ...routeContent, | ||
| 290 | showcase: e.target.value, | ||
| 291 | }); | ||
| 292 | }} /> | ||
| 293 | </div> | ||
| 294 | <div id='modview-route-description' style={{ height: "180px", gridColumn: "1 / span 5" }}> | ||
| 295 | <span>Description:</span> | ||
| 296 | <textarea value={routeContent.description} onChange={(e) => { | ||
| 297 | setRouteContent({ | ||
| 298 | ...routeContent, | ||
| 299 | description: e.target.value, | ||
| 300 | }); | ||
| 301 | setMd(routeContent.description); | ||
| 302 | }} /> | ||
| 303 | </div> | ||
| 304 | <button style={{ gridColumn: "2 / span 3", height: "40px" }} onClick={_create_map_summary_route}>Apply</button> | ||
| 305 | |||
| 306 | <div id='modview-md'> | ||
| 307 | <span>Markdown preview</span> | ||
| 308 | <span><a href="https://commonmark.org/help/" rel="noreferrer" target='_blank'>documentation</a></span> | ||
| 309 | <span><a href="https://remarkjs.github.io/react-markdown/" rel="noreferrer" target='_blank'>demo</a></span> | ||
| 310 | <p> | ||
| 311 | <ReactMarkdown>{md} | ||
| 312 | </ReactMarkdown> | ||
| 313 | </p> | ||
| 314 | </div> | ||
| 315 | </div> | ||
| 316 | ) | ||
| 317 | } | ||
| 318 | </div> | ||
| 319 | |||
| 320 | </div> | ||
| 321 | ); | ||
| 322 | }; | ||
| 323 | |||
| 324 | export default ModMenu; | ||
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..a8834b6 --- /dev/null +++ b/frontend/src/components/Sidebar.tsx | |||
| @@ -0,0 +1,183 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { Link, useLocation } from 'react-router-dom'; | ||
| 3 | |||
| 4 | import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, NewsIcon, PortalIcon, SearchIcon, TableIcon } from '../images/Images'; | ||
| 5 | import Login from './Login'; | ||
| 6 | import { UserProfile } from '../types/Profile'; | ||
| 7 | import { Search } from '../types/Search'; | ||
| 8 | import { API } from '../api/Api'; | ||
| 9 | import "../css/Sidebar.css"; | ||
| 10 | |||
| 11 | interface SidebarProps { | ||
| 12 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; | ||
| 13 | profile?: UserProfile; | ||
| 14 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; | ||
| 15 | }; | ||
| 16 | |||
| 17 | const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile }) => { | ||
| 18 | |||
| 19 | const [searchData, setSearchData] = React.useState<Search | undefined>(undefined); | ||
| 20 | const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); | ||
| 21 | const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); | ||
| 22 | |||
| 23 | const location = useLocation(); | ||
| 24 | const path = location.pathname; | ||
| 25 | |||
| 26 | const handle_sidebar_click = (clicked_sidebar_idx: number) => { | ||
| 27 | const btn = document.querySelectorAll("button.sidebar-button"); | ||
| 28 | if (isSidebarOpen) { setSidebarOpen(false); _handle_sidebar_hide() } | ||
| 29 | // clusterfuck | ||
| 30 | btn.forEach((e, i) => { | ||
| 31 | btn[i].classList.remove("sidebar-button-selected") | ||
| 32 | btn[i].classList.add("sidebar-button-deselected") | ||
| 33 | }) | ||
| 34 | btn[clicked_sidebar_idx].classList.add("sidebar-button-selected") | ||
| 35 | btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected") | ||
| 36 | }; | ||
| 37 | |||
| 38 | const _handle_sidebar_hide = () => { | ||
| 39 | var btn = document.querySelectorAll("button.sidebar-button") as NodeListOf<HTMLElement> | ||
| 40 | const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement> | ||
| 41 | const side = document.querySelector("#sidebar-list") as HTMLElement; | ||
| 42 | const searchbar = document.querySelector("#searchbar") as HTMLInputElement; | ||
| 43 | |||
| 44 | if (isSidebarOpen) { | ||
| 45 | if (profile) { | ||
| 46 | const login = document.querySelectorAll(".login>button")[1] as HTMLElement; | ||
| 47 | login.style.opacity = "1" | ||
| 48 | } | ||
| 49 | setSidebarOpen(false); | ||
| 50 | side.style.width = "320px" | ||
| 51 | btn.forEach((e, i) => { | ||
| 52 | e.style.width = "310px" | ||
| 53 | e.style.padding = "0.4em 0 0 11px" | ||
| 54 | setTimeout(() => { | ||
| 55 | span[i].style.opacity = "1" | ||
| 56 | }, 100) | ||
| 57 | }) | ||
| 58 | side.style.zIndex = "2" | ||
| 59 | } else { | ||
| 60 | if (profile) { | ||
| 61 | const login = document.querySelectorAll(".login>button")[1] as HTMLElement; | ||
| 62 | login.style.opacity = "0" | ||
| 63 | } | ||
| 64 | setSidebarOpen(true) | ||
| 65 | side.style.width = "40px"; | ||
| 66 | searchbar.focus(); | ||
| 67 | btn.forEach((e, i) => { | ||
| 68 | e.style.width = "40px" | ||
| 69 | e.style.padding = "0.4em 0 0 5px" | ||
| 70 | span[i].style.opacity = "0" | ||
| 71 | }) | ||
| 72 | setTimeout(() => { | ||
| 73 | side.style.zIndex = "0" | ||
| 74 | }, 300); | ||
| 75 | } | ||
| 76 | }; | ||
| 77 | |||
| 78 | const _handle_sidebar_lock = () => { | ||
| 79 | if (!isSidebarLocked) { | ||
| 80 | _handle_sidebar_hide() | ||
| 81 | setIsSidebarLocked(true); | ||
| 82 | setTimeout(() => setIsSidebarLocked(false), 300); | ||
| 83 | } | ||
| 84 | }; | ||
| 85 | |||
| 86 | const _handle_search_change = async (q: string) => { | ||
| 87 | const searchResponse = await API.get_search(q); | ||
| 88 | setSearchData(searchResponse); | ||
| 89 | }; | ||
| 90 | |||
| 91 | React.useEffect(() => { | ||
| 92 | if (path === "/") { handle_sidebar_click(1) } | ||
| 93 | else if (path.includes("news")) { handle_sidebar_click(2) } | ||
| 94 | else if (path.includes("games")) { handle_sidebar_click(3) } | ||
| 95 | else if (path.includes("leaderboards")) { handle_sidebar_click(4) } | ||
| 96 | else if (path.includes("scorelog")) { handle_sidebar_click(5) } | ||
| 97 | else if (path.includes("profile")) { handle_sidebar_click(6) } | ||
| 98 | else if (path.includes("rules")) { handle_sidebar_click(8) } | ||
| 99 | else if (path.includes("about")) { handle_sidebar_click(9) } | ||
| 100 | }, [path]); | ||
| 101 | |||
| 102 | return ( | ||
| 103 | <div id='sidebar'> | ||
| 104 | <Link to="/" tabIndex={-1}> | ||
| 105 | <div id='logo'> {/* logo */} | ||
| 106 | <img src={LogoIcon} alt="" height={"80px"} /> | ||
| 107 | <div id='logo-text'> | ||
| 108 | <span><b>PORTAL 2</b></span><br /> | ||
| 109 | <span>Least Portals</span> | ||
| 110 | </div> | ||
| 111 | </div> | ||
| 112 | </Link> | ||
| 113 | <div id='sidebar-list'> {/* List */} | ||
| 114 | <div id='sidebar-toplist'> {/* Top */} | ||
| 115 | |||
| 116 | <button className='sidebar-button' onClick={() => _handle_sidebar_lock()}><img src={SearchIcon} alt="" /><span>Search</span></button> | ||
| 117 | |||
| 118 | <span></span> | ||
| 119 | |||
| 120 | <Link to="/" tabIndex={-1}> | ||
| 121 | <button className='sidebar-button'><img src={HomeIcon} alt="homepage" /><span>Home Page</span></button> | ||
| 122 | </Link> | ||
| 123 | |||
| 124 | <Link to="/news" tabIndex={-1}> | ||
| 125 | <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button> | ||
| 126 | </Link> | ||
| 127 | |||
| 128 | <Link to="/games" tabIndex={-1}> | ||
| 129 | <button className='sidebar-button'><img src={PortalIcon} alt="games" /><span>Games</span></button> | ||
| 130 | </Link> | ||
| 131 | |||
| 132 | <Link to="/leaderboards" tabIndex={-1}> | ||
| 133 | <button className='sidebar-button'><img src={FlagIcon} alt="leaderboards" /><span>Leaderboards</span></button> | ||
| 134 | </Link> | ||
| 135 | |||
| 136 | <Link to="/scorelog" tabIndex={-1}> | ||
| 137 | <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score Logs</span></button> | ||
| 138 | </Link> | ||
| 139 | </div> | ||
| 140 | <div id='sidebar-bottomlist'> | ||
| 141 | <span></span> | ||
| 142 | |||
| 143 | <Login setToken={setToken} profile={profile} setProfile={setProfile} /> | ||
| 144 | |||
| 145 | <Link to="/rules" tabIndex={-1}> | ||
| 146 | <button className='sidebar-button'><img src={BookIcon} alt="leaderboardrules" /><span>Leaderboard Rules</span></button> | ||
| 147 | </Link> | ||
| 148 | |||
| 149 | <Link to="/about" tabIndex={-1}> | ||
| 150 | <button className='sidebar-button'><img src={HelpIcon} alt="aboutp2lp" /><span>About P2LP</span></button> | ||
| 151 | </Link> | ||
| 152 | </div> | ||
| 153 | </div> | ||
| 154 | <div> | ||
| 155 | <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} /> | ||
| 156 | |||
| 157 | <div id='search-data'> | ||
| 158 | |||
| 159 | {searchData?.maps.map((q, index) => ( | ||
| 160 | <Link to={`/maps/${q.id}`} className='search-map' key={index}> | ||
| 161 | <span>{q.game}</span> | ||
| 162 | <span>{q.chapter}</span> | ||
| 163 | <span>{q.map}</span> | ||
| 164 | </Link> | ||
| 165 | ))} | ||
| 166 | {searchData?.players.map((q, index) => | ||
| 167 | ( | ||
| 168 | <Link to={ | ||
| 169 | profile && q.steam_id === profile.steam_id ? `/profile` : | ||
| 170 | `/users/${q.steam_id}` | ||
| 171 | } className='search-player' key={index}> | ||
| 172 | <img src={q.avatar_link} alt='pfp'></img> | ||
| 173 | <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span> | ||
| 174 | </Link> | ||
| 175 | ))} | ||
| 176 | |||
| 177 | </div> | ||
| 178 | </div> | ||
| 179 | </div> | ||
| 180 | ); | ||
| 181 | }; | ||
| 182 | |||
| 183 | export default Sidebar; | ||
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx new file mode 100644 index 0000000..b8f0087 --- /dev/null +++ b/frontend/src/components/Summary.tsx | |||
| @@ -0,0 +1,169 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import ReactMarkdown from 'react-markdown'; | ||
| 3 | |||
| 4 | import { MapSummary } from '../types/Map'; | ||
| 5 | import "../css/Maps.css" | ||
| 6 | |||
| 7 | interface SummaryProps { | ||
| 8 | selectedRun: number | ||
| 9 | setSelectedRun: (x: number) => void; | ||
| 10 | data: MapSummary; | ||
| 11 | } | ||
| 12 | |||
| 13 | const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data }) => { | ||
| 14 | |||
| 15 | const [selectedCategory, setSelectedCategory] = React.useState<number>(1); | ||
| 16 | const [historySelected, setHistorySelected] = React.useState<boolean>(false); | ||
| 17 | |||
| 18 | function _select_run(x: number, y: number) { | ||
| 19 | let r = document.querySelectorAll("button.record"); | ||
| 20 | r.forEach(e => (e as HTMLElement).style.backgroundColor = "#2b2e46"); | ||
| 21 | (r[x] as HTMLElement).style.backgroundColor = "#161723" | ||
| 22 | |||
| 23 | |||
| 24 | if (data && data.summary.routes.length !== 0 && data.summary.routes.length !== 0) { | ||
| 25 | if (y === 2) { x += data.summary.routes.filter(e => e.category.id < 2).length } | ||
| 26 | if (y === 3) { x += data.summary.routes.filter(e => e.category.id < 3).length } | ||
| 27 | if (y === 4) { x += data.summary.routes.filter(e => e.category.id < 4).length } | ||
| 28 | setSelectedRun(x); | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | function _get_youtube_id(url: string): string { | ||
| 33 | const urlArray = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/); | ||
| 34 | return (urlArray[2] !== undefined) ? urlArray[2].split(/[^0-9a-z_]/i)[0] : urlArray[0]; | ||
| 35 | }; | ||
| 36 | |||
| 37 | function _category_change() { | ||
| 38 | const btn = document.querySelectorAll("#section3 #category span button"); | ||
| 39 | btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); | ||
| 40 | (btn[selectedCategory - 1] as HTMLElement).style.backgroundColor = "#202232"; | ||
| 41 | }; | ||
| 42 | |||
| 43 | function _history_change() { | ||
| 44 | const btn = document.querySelectorAll("#section3 #history span button"); | ||
| 45 | btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); | ||
| 46 | (historySelected ? btn[1] as HTMLElement : btn[0] as HTMLElement).style.backgroundColor = "#202232"; | ||
| 47 | }; | ||
| 48 | |||
| 49 | React.useEffect(() => { | ||
| 50 | _history_change(); | ||
| 51 | }, [historySelected]); | ||
| 52 | |||
| 53 | React.useEffect(() => { | ||
| 54 | _category_change(); | ||
| 55 | }, [selectedCategory]); | ||
| 56 | |||
| 57 | React.useEffect(() => { | ||
| 58 | _select_run(0, selectedCategory); | ||
| 59 | }, []); | ||
| 60 | |||
| 61 | return ( | ||
| 62 | <> | ||
| 63 | <section id='section3' className='summary1'> | ||
| 64 | <div id='category' | ||
| 65 | style={data.map.image === "" ? { backgroundColor: "#202232" } : {}}> | ||
| 66 | <img src={data.map.image} alt="" id='category-image'></img> | ||
| 67 | <p><span className='portal-count'>{data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].history.score_count}</span> | ||
| 68 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].history.score_count === 1 ? ` portal` : ` portals`}</p> | ||
| 69 | <span> | ||
| 70 | <button onClick={() => setSelectedCategory(1)}>CM</button> | ||
| 71 | <button onClick={() => setSelectedCategory(2)}>NoSLA</button> | ||
| 72 | {data.map.is_coop ? <button onClick={() => setSelectedCategory(3)}>SLA</button> | ||
| 73 | : <button onClick={() => setSelectedCategory(3)}>Inbounds SLA</button>} | ||
| 74 | <button onClick={() => setSelectedCategory(4)}>Any%</button> | ||
| 75 | </span> | ||
| 76 | |||
| 77 | </div> | ||
| 78 | |||
| 79 | <div id='history'> | ||
| 80 | |||
| 81 | <div style={{ display: historySelected ? "none" : "block" }}> | ||
| 82 | {data.summary.routes.filter(e => e.category.id === selectedCategory).length === 0 ? <h5>There are no records for this map.</h5> : | ||
| 83 | <> | ||
| 84 | <div className='record-top'> | ||
| 85 | <span>Date</span> | ||
| 86 | <span>Record</span> | ||
| 87 | <span>First completion</span> | ||
| 88 | </div> | ||
| 89 | <hr /> | ||
| 90 | <div id='records'> | ||
| 91 | |||
| 92 | {data.summary.routes | ||
| 93 | .sort((a, b) => a.history.score_count - b.history.score_count) | ||
| 94 | .filter(e => e.category.id === selectedCategory) | ||
| 95 | .map((r, index) => ( | ||
| 96 | <button className='record' key={index} onClick={() => { | ||
| 97 | _select_run(index, r.category.id); | ||
| 98 | }}> | ||
| 99 | <span>{new Date(r.history.date).toLocaleDateString( | ||
| 100 | "en-US", { month: 'long', day: 'numeric', year: 'numeric' } | ||
| 101 | )}</span> | ||
| 102 | <span>{r.history.score_count}</span> | ||
| 103 | <span>{r.history.runner_name}</span> | ||
| 104 | </button> | ||
| 105 | ))} | ||
| 106 | </div> | ||
| 107 | </> | ||
| 108 | } | ||
| 109 | </div> | ||
| 110 | |||
| 111 | <div style={{ display: historySelected ? "block" : "none" }}> | ||
| 112 | {data.summary.routes.filter(e => e.category.id === selectedCategory).length === 0 ? <h5>There are no records for this map.</h5> : | ||
| 113 | <div id='graph'> | ||
| 114 | {/* <div>{graph(1)}</div> | ||
| 115 | <div>{graph(2)}</div> | ||
| 116 | <div>{graph(3)}</div> */} | ||
| 117 | </div> | ||
| 118 | } | ||
| 119 | </div> | ||
| 120 | <span> | ||
| 121 | <button onClick={() => setHistorySelected(false)}>List</button> | ||
| 122 | <button onClick={() => setHistorySelected(true)}>Graph</button> | ||
| 123 | </span> | ||
| 124 | </div> | ||
| 125 | |||
| 126 | |||
| 127 | </section> | ||
| 128 | <section id='section4' className='summary1'> | ||
| 129 | <div id='difficulty'> | ||
| 130 | <span>Difficulty</span> | ||
| 131 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].rating === 0 ? (<span>N/A</span>) : null} | ||
| 132 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].rating === 1 ? (<span style={{ color: "lime" }}>Very easy</span>) : null} | ||
| 133 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].rating === 2 ? (<span style={{ color: "green" }}>Easy</span>) : null} | ||
| 134 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].rating === 3 ? (<span style={{ color: "yellow" }}>Medium</span>) : null} | ||
| 135 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].rating === 4 ? (<span style={{ color: "orange" }}>Hard</span>) : null} | ||
| 136 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].rating === 5 ? (<span style={{ color: "red" }}>Very hard</span>) : null} | ||
| 137 | <div> | ||
| 138 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].rating === 1 ? (<div className='difficulty-rating' style={{ backgroundColor: "lime" }}></div>) : (<div className='difficulty-rating'></div>)} | ||
| 139 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].rating === 2 ? (<div className='difficulty-rating' style={{ backgroundColor: "green" }}></div>) : (<div className='difficulty-rating'></div>)} | ||
| 140 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].rating === 3 ? (<div className='difficulty-rating' style={{ backgroundColor: "yellow" }}></div>) : (<div className='difficulty-rating'></div>)} | ||
| 141 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].rating === 4 ? (<div className='difficulty-rating' style={{ backgroundColor: "orange" }}></div>) : (<div className='difficulty-rating'></div>)} | ||
| 142 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].rating === 5 ? (<div className='difficulty-rating' style={{ backgroundColor: "red" }}></div>) : (<div className='difficulty-rating'></div>)} | ||
| 143 | </div> | ||
| 144 | </div> | ||
| 145 | <div id='count'> | ||
| 146 | <span>Completion count</span> | ||
| 147 | <div>{selectedCategory === 1 ? data.summary.routes[selectedRun].completion_count : "N/A"}</div> | ||
| 148 | </div> | ||
| 149 | </section> | ||
| 150 | |||
| 151 | <section id='section5' className='summary1'> | ||
| 152 | <div id='description'> | ||
| 153 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].showcase !== "" ? | ||
| 154 | <iframe title='Showcase video' src={"https://www.youtube.com/embed/" + _get_youtube_id(data.summary.routes[selectedRun].showcase)}> </iframe> | ||
| 155 | : ""} | ||
| 156 | <h3>Route description</h3> | ||
| 157 | <span id='description-text'> | ||
| 158 | <ReactMarkdown> | ||
| 159 | {data.summary.routes.sort((a, b) => a.category.id - b.category.id)[selectedRun].description} | ||
| 160 | </ReactMarkdown> | ||
| 161 | </span> | ||
| 162 | </div> | ||
| 163 | </section> | ||
| 164 | |||
| 165 | </> | ||
| 166 | ); | ||
| 167 | }; | ||
| 168 | |||
| 169 | export default Summary; | ||
diff --git a/frontend/src/components/login.js b/frontend/src/components/login.js deleted file mode 100644 index f86ad44..0000000 --- a/frontend/src/components/login.js +++ /dev/null | |||
| @@ -1,61 +0,0 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { Link } from "react-router-dom"; | ||
| 3 | |||
| 4 | import "./login.css"; | ||
| 5 | import img1 from "../imgs/login.png" | ||
| 6 | import img2 from "../imgs/10.png" | ||
| 7 | import img3 from "../imgs/11.png" | ||
| 8 | |||
| 9 | |||
| 10 | export default function Login(prop) { | ||
| 11 | const {setToken,profile,setProfile} = prop | ||
| 12 | function login() { | ||
| 13 | window.location.href="https://lp.ardapektezol.com/api/v1/login" | ||
| 14 | } | ||
| 15 | function logout() { | ||
| 16 | setIsLoggedIn(false) | ||
| 17 | setProfile(null) | ||
| 18 | setToken(null) | ||
| 19 | fetch(`https://lp.ardapektezol.com/api/v1/token`,{'method':'DELETE'}) | ||
| 20 | .then(r=>window.location.href="/") | ||
| 21 | } | ||
| 22 | const [isLoggedIn, setIsLoggedIn] = React.useState(false); | ||
| 23 | React.useEffect(() => { | ||
| 24 | fetch(`https://lp.ardapektezol.com/api/v1/token`) | ||
| 25 | .then(r => r.json()) | ||
| 26 | .then(d => { | ||
| 27 | if (d.data != null) { | ||
| 28 | setToken(d.data.token) | ||
| 29 | } | ||
| 30 | }) | ||
| 31 | }, []); | ||
| 32 | |||
| 33 | |||
| 34 | React.useEffect(() => { | ||
| 35 | if(profile!==null){setIsLoggedIn(true)} | ||
| 36 | }, [profile]); | ||
| 37 | |||
| 38 | return ( | ||
| 39 | <> | ||
| 40 | {isLoggedIn ? ( | ||
| 41 | <Link to="/profile" tabIndex={-1} className='login'> | ||
| 42 | <button className='sidebar-button'> | ||
| 43 | <img src={profile.avatar_link} alt="" /> | ||
| 44 | <span>{profile.user_name}</span> | ||
| 45 | </button> | ||
| 46 | <button className='sidebar-button' onClick={logout}><img src={img3} alt="" /><span></span></button> | ||
| 47 | </Link> | ||
| 48 | ) : ( | ||
| 49 | <Link tabIndex={-1} className='login' > | ||
| 50 | <button className='sidebar-button' onClick={login}> | ||
| 51 | <img src={img2} alt="" /> | ||
| 52 | <span><img src={img1} alt="Sign in through Steam" /></span> | ||
| 53 | </button> | ||
| 54 | <button className='sidebar-button' disabled><span></span></button> | ||
| 55 | </Link> | ||
| 56 | )} | ||
| 57 | </> | ||
| 58 | ) | ||
| 59 | } | ||
| 60 | |||
| 61 | |||
diff --git a/frontend/src/components/main.css b/frontend/src/components/main.css deleted file mode 100644 index 48e6379..0000000 --- a/frontend/src/components/main.css +++ /dev/null | |||
| @@ -1,17 +0,0 @@ | |||
| 1 | |||
| 2 | main { | ||
| 3 | overflow: auto; | ||
| 4 | overflow-x: hidden; | ||
| 5 | position: relative; | ||
| 6 | |||
| 7 | width: calc(100% - 380px); | ||
| 8 | height: 100vh; | ||
| 9 | left: 350px; | ||
| 10 | |||
| 11 | padding-right: 30px; | ||
| 12 | |||
| 13 | font-size: 40px; | ||
| 14 | font-family: BarlowSemiCondensed-Regular; | ||
| 15 | color: #cdcfdf; | ||
| 16 | |||
| 17 | } | ||
diff --git a/frontend/src/components/main.js b/frontend/src/components/main.js deleted file mode 100644 index b359105..0000000 --- a/frontend/src/components/main.js +++ /dev/null | |||
| @@ -1,17 +0,0 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | |||
| 3 | import "../App.css" | ||
| 4 | import "./main.css"; | ||
| 5 | import { Link } from 'react-router-dom'; | ||
| 6 | |||
| 7 | export default function Main(props) { | ||
| 8 | |||
| 9 | |||
| 10 | return ( | ||
| 11 | <main> | ||
| 12 | <h1>{props.text}</h1> | ||
| 13 | </main> | ||
| 14 | ) | ||
| 15 | } | ||
| 16 | |||
| 17 | |||
diff --git a/frontend/src/components/news.css b/frontend/src/components/news.css deleted file mode 100644 index 102e9ba..0000000 --- a/frontend/src/components/news.css +++ /dev/null | |||
| @@ -1,29 +0,0 @@ | |||
| 1 | .news-container { | ||
| 2 | background-color: #2A2D40; | ||
| 3 | border-radius: 24px; | ||
| 4 | font-size: 18px; | ||
| 5 | overflow: hidden; | ||
| 6 | margin-bottom: 10px; | ||
| 7 | } | ||
| 8 | |||
| 9 | .news-title { | ||
| 10 | padding: 20px; | ||
| 11 | font-family: BarlowSemiCondensed-SemiBold; | ||
| 12 | font-size: 22px; | ||
| 13 | } | ||
| 14 | |||
| 15 | .news-description-div { | ||
| 16 | margin: 0px 20px; | ||
| 17 | padding: 8px 0px; | ||
| 18 | } | ||
| 19 | |||
| 20 | .news-title-header { | ||
| 21 | background-color: #2B2E46; | ||
| 22 | padding: 10px 0px; | ||
| 23 | } | ||
| 24 | |||
| 25 | .news-container>span { | ||
| 26 | font-size: 18px; | ||
| 27 | font-family: BarlowSemiCondensed-Regular; | ||
| 28 | line-height: 0px; | ||
| 29 | } \ No newline at end of file | ||
diff --git a/frontend/src/components/news.js b/frontend/src/components/news.js deleted file mode 100644 index 93e6be0..0000000 --- a/frontend/src/components/news.js +++ /dev/null | |||
| @@ -1,21 +0,0 @@ | |||
| 1 | import React, { useEffect, useRef, useState } from 'react'; | ||
| 2 | import { useLocation, Link } from "react-router-dom"; | ||
| 3 | |||
| 4 | import "./news.css" | ||
| 5 | |||
| 6 | export default function News({newsInfo}) { | ||
| 7 | // const { token } = prop | ||
| 8 | const [news, setNews] = React.useState(null); | ||
| 9 | const location = useLocation(); | ||
| 10 | |||
| 11 | return ( | ||
| 12 | <div className='news-container'> | ||
| 13 | <div className='news-title-header'> | ||
| 14 | <span className='news-title'>{newsInfo.title}</span> | ||
| 15 | </div> | ||
| 16 | <div className='news-description-div'> | ||
| 17 | <span className='description'>{newsInfo.short_description}</span> | ||
| 18 | </div> | ||
| 19 | </div> | ||
| 20 | ) | ||
| 21 | } \ No newline at end of file | ||
diff --git a/frontend/src/components/pages/about.css b/frontend/src/components/pages/about.css deleted file mode 100644 index 0dec300..0000000 --- a/frontend/src/components/pages/about.css +++ /dev/null | |||
| @@ -1,17 +0,0 @@ | |||
| 1 | |||
| 2 | #about { | ||
| 3 | overflow: auto; | ||
| 4 | overflow-x: hidden; | ||
| 5 | position: relative; | ||
| 6 | |||
| 7 | width: calc(100% - 380px); | ||
| 8 | height: 100vh; | ||
| 9 | left: 350px; | ||
| 10 | |||
| 11 | padding-right: 30px; | ||
| 12 | |||
| 13 | font-size: 40px; | ||
| 14 | font-family: BarlowSemiCondensed-Regular; | ||
| 15 | color: #cdcfdf; | ||
| 16 | |||
| 17 | } | ||
diff --git a/frontend/src/components/pages/about.js b/frontend/src/components/pages/about.js deleted file mode 100644 index 11b065d..0000000 --- a/frontend/src/components/pages/about.js +++ /dev/null | |||
| @@ -1,32 +0,0 @@ | |||
| 1 | import React, { useState, useEffect } from 'react'; | ||
| 2 | import ReactMarkdown from 'react-markdown'; | ||
| 3 | |||
| 4 | import "./about.css"; | ||
| 5 | |||
| 6 | export default function About() { | ||
| 7 | const [aboutText, setAboutText] = useState(''); | ||
| 8 | |||
| 9 | useEffect(() => { | ||
| 10 | const fetchReadme = async () => { | ||
| 11 | try { | ||
| 12 | const response = await fetch( | ||
| 13 | 'https://raw.githubusercontent.com/pektezol/leastportalshub/main/README.md' | ||
| 14 | ); | ||
| 15 | if (!response.ok) { | ||
| 16 | throw new Error('Failed to fetch README'); | ||
| 17 | } | ||
| 18 | const readmeText = await response.text(); | ||
| 19 | setAboutText(readmeText); | ||
| 20 | } catch (error) { | ||
| 21 | console.error('Error fetching README:', error); | ||
| 22 | } | ||
| 23 | }; | ||
| 24 | fetchReadme(); | ||
| 25 | }, []); | ||
| 26 | |||
| 27 | return ( | ||
| 28 | <div id="about"> | ||
| 29 | <ReactMarkdown>{aboutText}</ReactMarkdown> | ||
| 30 | </div> | ||
| 31 | ); | ||
| 32 | }; | ||
diff --git a/frontend/src/components/pages/game.js b/frontend/src/components/pages/game.js deleted file mode 100644 index 301e035..0000000 --- a/frontend/src/components/pages/game.js +++ /dev/null | |||
| @@ -1,46 +0,0 @@ | |||
| 1 | import React, { useEffect, useRef, useState } from 'react'; | ||
| 2 | import { useLocation, Link } from "react-router-dom"; | ||
| 3 | |||
| 4 | import "./games.css" | ||
| 5 | |||
| 6 | export default function GameEntry({ gameInfo }) { | ||
| 7 | const [gameEntry, setGameEntry] = React.useState(null); | ||
| 8 | const location = useLocation(); | ||
| 9 | |||
| 10 | const gameInfoCats = gameInfo.category_portals; | ||
| 11 | |||
| 12 | useEffect(() => { | ||
| 13 | gameInfoCats.forEach(catInfo => { | ||
| 14 | const itemBody = document.createElement("div"); | ||
| 15 | const itemTitle = document.createElement("span"); | ||
| 16 | const spacing = document.createElement("br"); | ||
| 17 | const itemNum = document.createElement("span"); | ||
| 18 | |||
| 19 | itemTitle.innerText = catInfo.category.name; | ||
| 20 | itemNum.innerText = catInfo.portal_count; | ||
| 21 | itemTitle.classList.add("games-page-item-body-item-title"); | ||
| 22 | itemNum.classList.add("games-page-item-body-item-num"); | ||
| 23 | itemBody.appendChild(itemTitle); | ||
| 24 | itemBody.appendChild(spacing); | ||
| 25 | itemBody.appendChild(itemNum); | ||
| 26 | itemBody.className = "games-page-item-body-item"; | ||
| 27 | |||
| 28 | // itemBody.innerHTML = ` | ||
| 29 | // <span className='games-page-item-body-item-title'>${catInfo.category.name}</span><br /> | ||
| 30 | // <span className='games-page-item-body-item-num'>${catInfo.portal_count}</span>` | ||
| 31 | |||
| 32 | document.getElementById(`${gameInfo.id}`).appendChild(itemBody); | ||
| 33 | }); | ||
| 34 | }) | ||
| 35 | |||
| 36 | return ( | ||
| 37 | <Link to={"/games/" + gameInfo.id}><div className='games-page-item'> | ||
| 38 | <div className='games-page-item-header'> | ||
| 39 | <div style={{backgroundImage: `url(${gameInfo.image})`}} className='games-page-item-header-img'></div> | ||
| 40 | <span><b>{gameInfo.name}</b></span> | ||
| 41 | </div> | ||
| 42 | <div id={gameInfo.id} className='games-page-item-body'> | ||
| 43 | </div> | ||
| 44 | </div></Link> | ||
| 45 | ) | ||
| 46 | } | ||
diff --git a/frontend/src/components/pages/games.js b/frontend/src/components/pages/games.js deleted file mode 100644 index 75b5e44..0000000 --- a/frontend/src/components/pages/games.js +++ /dev/null | |||
| @@ -1,62 +0,0 @@ | |||
| 1 | import React, { useEffect, useState } from 'react'; | ||
| 2 | import { useLocation, Link } from "react-router-dom"; | ||
| 3 | |||
| 4 | import "./games.css" | ||
| 5 | import GameEntry from './game'; | ||
| 6 | |||
| 7 | export default function Games(prop) { | ||
| 8 | const { token } = prop; | ||
| 9 | const [games, setGames] = useState([]); | ||
| 10 | const location = useLocation(); | ||
| 11 | |||
| 12 | useEffect(() => { | ||
| 13 | document.querySelectorAll(".games-page-item-body").forEach((game, index) => { | ||
| 14 | game.innerHTML = ""; | ||
| 15 | }) | ||
| 16 | |||
| 17 | const fetchGames = async () => { | ||
| 18 | try { | ||
| 19 | const response = await fetch("https://lp.ardapektezol.com/api/v1/games", { | ||
| 20 | headers: { | ||
| 21 | 'Authorization': token | ||
| 22 | } | ||
| 23 | }); | ||
| 24 | |||
| 25 | const data = await response.json(); | ||
| 26 | setGames(data.data); | ||
| 27 | pageLoad(); | ||
| 28 | } catch (err) { | ||
| 29 | console.error("Error fetching games:", err); | ||
| 30 | } | ||
| 31 | }; | ||
| 32 | |||
| 33 | fetchGames(); | ||
| 34 | |||
| 35 | function pageLoad() { | ||
| 36 | const loaders = document.querySelectorAll(".loader"); | ||
| 37 | loaders.forEach((loader) => { | ||
| 38 | loader.style.display = "none"; | ||
| 39 | }); | ||
| 40 | } | ||
| 41 | }, [token]); | ||
| 42 | |||
| 43 | return ( | ||
| 44 | <div className='games-page'> | ||
| 45 | <section className='games-page-header'> | ||
| 46 | <span><b>Games list</b></span> | ||
| 47 | </section> | ||
| 48 | |||
| 49 | <section> | ||
| 50 | <div className='games-page-content'> | ||
| 51 | <div className='games-page-item-content'> | ||
| 52 | <div className='loader loader-game'></div> | ||
| 53 | <div className='loader loader-game'></div> | ||
| 54 | {games.map((game, index) => ( | ||
| 55 | <GameEntry gameInfo={game} key={index} /> | ||
| 56 | ))} | ||
| 57 | </div> | ||
| 58 | </div> | ||
| 59 | </section> | ||
| 60 | </div> | ||
| 61 | ); | ||
| 62 | } | ||
diff --git a/frontend/src/components/pages/home.css b/frontend/src/components/pages/home.css deleted file mode 100644 index e5a8eab..0000000 --- a/frontend/src/components/pages/home.css +++ /dev/null | |||
| @@ -1,92 +0,0 @@ | |||
| 1 | * { | ||
| 2 | scrollbar-width: thin; | ||
| 3 | } | ||
| 4 | |||
| 5 | .homepage-panel { | ||
| 6 | background-color: #202232; | ||
| 7 | margin: 10px 10px; | ||
| 8 | padding: 10px; | ||
| 9 | border-radius: 24px; | ||
| 10 | overflow: hidden; | ||
| 11 | flex: 1 1 100%; | ||
| 12 | align-items: stretch; | ||
| 13 | width: 100%; | ||
| 14 | } | ||
| 15 | |||
| 16 | .homepage-panel-title-div { | ||
| 17 | background-color: #2B2E46; | ||
| 18 | width: fit-content; | ||
| 19 | padding: 5px 18px; | ||
| 20 | border-radius: 200px; | ||
| 21 | font-family: BarlowSemiCondensed-SemiBold; | ||
| 22 | font-size: 32px; | ||
| 23 | margin-bottom: 10px; | ||
| 24 | } | ||
| 25 | |||
| 26 | .stats-div { | ||
| 27 | background-color: #2B2E46; | ||
| 28 | border-radius: 24px; | ||
| 29 | text-align: center; | ||
| 30 | display: grid; | ||
| 31 | padding: 10px 0px; | ||
| 32 | width: 100%; | ||
| 33 | } | ||
| 34 | |||
| 35 | .stats-div span { | ||
| 36 | font-family: BarlowSemiCondensed-Regular; | ||
| 37 | font-size: 18px; | ||
| 38 | } | ||
| 39 | |||
| 40 | .stats-div span>b { | ||
| 41 | font-family: BarlowSemiCondensed-SemiBold; | ||
| 42 | font-size: 42px; | ||
| 43 | } | ||
| 44 | |||
| 45 | .record-title div { | ||
| 46 | --padding: 20px; | ||
| 47 | width: calc(100% - calc(var(--padding * 2))); | ||
| 48 | height: 32px; | ||
| 49 | border-radius: 200px; | ||
| 50 | font-size: 18px; | ||
| 51 | display: grid; | ||
| 52 | grid-template-columns: calc(20% - 3.6px) calc(25% - 4.5px) calc(15% - 2.7px) calc(15% - 2.7px) calc(25% - 4.5px); | ||
| 53 | text-align: center; | ||
| 54 | padding: 0px var(--padding); | ||
| 55 | vertical-align: middle; | ||
| 56 | align-items: center; | ||
| 57 | font-family: BarlowSemiCondensed-SemiBold; | ||
| 58 | } | ||
| 59 | |||
| 60 | .record-title::after { | ||
| 61 | content: ""; | ||
| 62 | display: flex; | ||
| 63 | width: 100%; | ||
| 64 | height: 3px; | ||
| 65 | background-color: #2B2E46; | ||
| 66 | margin-bottom: 5px; | ||
| 67 | } | ||
| 68 | |||
| 69 | .recommended-map-img { | ||
| 70 | width: 250px; | ||
| 71 | border-radius: 19px; | ||
| 72 | margin-bottom: 0; | ||
| 73 | /* border: 7px solid #2B2E46; */ | ||
| 74 | background-size: cover; | ||
| 75 | background-position-x: 50%; | ||
| 76 | } | ||
| 77 | |||
| 78 | .difficulty-bar-home { | ||
| 79 | width: 100%; | ||
| 80 | display: grid; | ||
| 81 | grid-template-columns: 20% 20% 20% 20% 20%; | ||
| 82 | align-items: center; | ||
| 83 | margin: 0px; | ||
| 84 | margin-top: 7px; | ||
| 85 | } | ||
| 86 | |||
| 87 | .difficulty-point { | ||
| 88 | background: #2B2E46; | ||
| 89 | height: 4px; | ||
| 90 | margin: 5px; | ||
| 91 | border-radius: 10px; | ||
| 92 | } | ||
diff --git a/frontend/src/components/pages/home.js b/frontend/src/components/pages/home.js deleted file mode 100644 index 0a46bec..0000000 --- a/frontend/src/components/pages/home.js +++ /dev/null | |||
| @@ -1,242 +0,0 @@ | |||
| 1 | import React, { useEffect, useState } from 'react'; | ||
| 2 | |||
| 3 | import "./home.css" | ||
| 4 | import News from '../news'; | ||
| 5 | import Record from '../record'; | ||
| 6 | |||
| 7 | export default function Homepage({ token }) { | ||
| 8 | const [profile, setProfile] = useState(null); | ||
| 9 | |||
| 10 | // useEffect(() => { | ||
| 11 | |||
| 12 | // if (!token) { | ||
| 13 | // return; | ||
| 14 | // } | ||
| 15 | |||
| 16 | // async function home() { | ||
| 17 | |||
| 18 | // const profileResponse = await fetch(`https://lp.ardapektezol.com/api/v1/profile`, { | ||
| 19 | // headers: { | ||
| 20 | // Authorization: token | ||
| 21 | // } | ||
| 22 | // }) | ||
| 23 | |||
| 24 | // const profileData = await profileResponse.json(); | ||
| 25 | |||
| 26 | // setProfile(profileData); | ||
| 27 | |||
| 28 | // const gamesResponse = await fetch("https://lp.ardapektezol.com/api/v1/games", { | ||
| 29 | // headers: { | ||
| 30 | // Authorization: token | ||
| 31 | // } | ||
| 32 | // }); | ||
| 33 | |||
| 34 | // const gamesData = await gamesResponse.json(); | ||
| 35 | |||
| 36 | // const recommendedMapImg = document.querySelector("#recommendedMapImg"); | ||
| 37 | |||
| 38 | // recommendedMapImg.style.backgroundImage = `url(${gamesData.data[0].image})` | ||
| 39 | |||
| 40 | // const column1 = document.querySelector("#column1"); | ||
| 41 | // const column2 = document.querySelector("#column2"); | ||
| 42 | |||
| 43 | // column2.style.height = column1.clientHeight + "px"; | ||
| 44 | |||
| 45 | // const panels = document.querySelectorAll(".homepage-panel"); | ||
| 46 | // panels.forEach(e => { | ||
| 47 | // // this is cuz react is silly | ||
| 48 | // if (e.innerHTML.includes('<div class="homepage-panel-title-div">')) { | ||
| 49 | // return | ||
| 50 | // } | ||
| 51 | // const title = e.getAttribute("title"); | ||
| 52 | |||
| 53 | // const titleDiv = document.createElement("div"); | ||
| 54 | // const titleSpan = document.createElement("span"); | ||
| 55 | |||
| 56 | // titleDiv.classList.add("homepage-panel-title-div") | ||
| 57 | |||
| 58 | // titleSpan.innerText = title | ||
| 59 | |||
| 60 | // titleDiv.appendChild(titleSpan) | ||
| 61 | // e.insertBefore(titleDiv, e.firstChild) | ||
| 62 | // }); | ||
| 63 | // } | ||
| 64 | // try { | ||
| 65 | // home(); | ||
| 66 | // } catch (e) { | ||
| 67 | // console.log("error while setting up home page:", e); | ||
| 68 | // } | ||
| 69 | |||
| 70 | // }, [token]); | ||
| 71 | |||
| 72 | const newsList = [ | ||
| 73 | { | ||
| 74 | "title": "Portal Saved on Container Ride", | ||
| 75 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 76 | }, | ||
| 77 | { | ||
| 78 | "title": "Portal Saved on Container Ride", | ||
| 79 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 80 | }, | ||
| 81 | { | ||
| 82 | "title": "Portal Saved on Container Ride", | ||
| 83 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 84 | }, | ||
| 85 | { | ||
| 86 | "title": "Portal Saved on Container Ride", | ||
| 87 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 88 | }, | ||
| 89 | { | ||
| 90 | "title": "Portal Saved on Container Ride", | ||
| 91 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 92 | }, | ||
| 93 | { | ||
| 94 | "title": "Portal Saved on Container Ride", | ||
| 95 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 96 | }, | ||
| 97 | { | ||
| 98 | "title": "Portal Saved on Container Ride", | ||
| 99 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 100 | }, | ||
| 101 | { | ||
| 102 | "title": "Portal Saved on Container Ride", | ||
| 103 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 104 | }, | ||
| 105 | { | ||
| 106 | "title": "Portal Saved on Container Ride", | ||
| 107 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 108 | }, | ||
| 109 | { | ||
| 110 | "title": "Portal Saved on Container Ride", | ||
| 111 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 112 | }, | ||
| 113 | { | ||
| 114 | "title": "Portal Saved on Container Ride", | ||
| 115 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 116 | }, | ||
| 117 | { | ||
| 118 | "title": "Portal Saved on Container Ride", | ||
| 119 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 120 | }, | ||
| 121 | { | ||
| 122 | "title": "Portal Saved on Container Ride", | ||
| 123 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 124 | }, | ||
| 125 | { | ||
| 126 | "title": "Portal Saved on Container Ride", | ||
| 127 | "short_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vehicula facilisis quam, non ultrices nisl aliquam at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas." | ||
| 128 | }, | ||
| 129 | ] | ||
| 130 | |||
| 131 | return ( | ||
| 132 | <main> | ||
| 133 | <section style={{ userSelect: "none", display: "flex" }}> | ||
| 134 | <h1 style={{ marginTop: "53.6px", fontSize: "80px", marginBottom: "15px" }}>Home</h1> | ||
| 135 | {profile ? | ||
| 136 | <div style={{ textAlign: "right", width: "100%", marginTop: "20px" }}> | ||
| 137 | <span style={{ fontSize: "25px" }}>Welcome back,</span><br /> | ||
| 138 | |||
| 139 | <span><b style={{ fontSize: "80px", transform: "translateY(-20px)", display: "block" }}>Wolfboy248</b></span> | ||
| 140 | </div> | ||
| 141 | : null} | ||
| 142 | </section> | ||
| 143 | |||
| 144 | <div style={{ display: "grid", gridTemplateColumns: "calc(50%) calc(50%)" }}> | ||
| 145 | <div id='column1' style={{ display: "flex", alignItems: "self-start", flexWrap: "wrap", alignContent: "start" }}> | ||
| 146 | {/* Column 1 */} | ||
| 147 | {profile ? | ||
| 148 | <section title="Your Profile" className='homepage-panel'> | ||
| 149 | <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "12px" }}> | ||
| 150 | <div className='stats-div'> | ||
| 151 | <span>Overall rank</span><br /> | ||
| 152 | <span><b>{profile.rankings.overall.rank > 0 ? "#" + profile.rankings.overall.rank : "No rank"}</b></span> | ||
| 153 | </div> | ||
| 154 | <div className='stats-div'> | ||
| 155 | <span>Singleplayer</span><br /> | ||
| 156 | <span style={{ fontSize: "22px" }}><b>{profile.rankings.singleplayer.rank > 0 ? "#" + profile.rankings.singleplayer.rank : "No rank"}</b> {profile.rankings.singleplayer.rank > 0 ? "(" + profile.rankings.singleplayer.completion_count + "/" + profile.rankings.singleplayer.completion_total + ")" : ""}</span> | ||
| 157 | </div> | ||
| 158 | <div className='stats-div'> | ||
| 159 | <span>Cooperative rank</span><br /> | ||
| 160 | <span style={{ fontSize: "22px" }}><b>{profile.rankings.cooperative.rank > 0 ? "#" + profile.rankings.cooperative.rank : "No rank"}</b> {profile.rankings.cooperative.rank > 0 ? "(" + profile.rankings.cooperative.completion_count + "/" + profile.rankings.cooperative.completion_total + ")" : ""}</span> | ||
| 161 | </div> | ||
| 162 | </div> | ||
| 163 | </section> | ||
| 164 | : null} | ||
| 165 | {profile ? | ||
| 166 | <section title="What's Next?" className='homepage-panel'> | ||
| 167 | <div style={{ display: "flex" }}> | ||
| 168 | <div className='recommended-map-img' id="recommendedMapImg"></div> | ||
| 169 | <div style={{ marginLeft: "12px", display: "block", width: "100%" }}> | ||
| 170 | <span style={{ fontFamily: "BarlowSemiCondensed-SemiBold", fontSize: "32px", width: "100%", display: "block" }}>Container Ride</span> | ||
| 171 | <span style={{ fontSize: "20px", display: "block" }}>Your Record: 4 portals</span> | ||
| 172 | <span style={{ fontFamily: "BarlowSemiCondensed-SemiBold", fontSize: "36px", width: "100%", display: "block" }}>World Record: 2 portals</span> | ||
| 173 | <div className='difficulty-bar-home'> | ||
| 174 | <div className='difficulty-point' style={{ backgroundColor: "#51C355" }}></div> | ||
| 175 | <div className='difficulty-point'></div> | ||
| 176 | <div className='difficulty-point'></div> | ||
| 177 | <div className='difficulty-point'></div> | ||
| 178 | <div className='difficulty-point'></div> | ||
| 179 | </div> | ||
| 180 | </div> | ||
| 181 | </div> | ||
| 182 | </section> | ||
| 183 | : null} | ||
| 184 | <section title="Newest Records" className='homepage-panel' style={{ height: profile ? "250px" : "960px" }}> | ||
| 185 | <div className='record-title'> | ||
| 186 | <div> | ||
| 187 | <span>Place</span> | ||
| 188 | <span style={{ textAlign: "left" }}>Runner</span> | ||
| 189 | <span>Portals</span> | ||
| 190 | <span>Time</span> | ||
| 191 | <span>Date</span> | ||
| 192 | </div> | ||
| 193 | </div> | ||
| 194 | <div style={{ overflowY: "scroll", height: "calc(100% - 90px)", paddingRight: "10px" }}> | ||
| 195 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 196 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 197 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 198 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 199 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 200 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 201 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 202 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 203 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 204 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 205 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 206 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 207 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 208 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 209 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 210 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 211 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 212 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 213 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 214 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 215 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 216 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 217 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 218 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 219 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 220 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 221 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 222 | <Record name={"Krzyhau"} portals={"2"} date={new Date("2024-05-21T08:45:00")} place={"2"} time={"20.20"}></Record> | ||
| 223 | </div> | ||
| 224 | </section> | ||
| 225 | </div> | ||
| 226 | {/* Column 2 */} | ||
| 227 | <div id='column2' style={{ display: "flex", alignItems: "stretch", height: "1000px" }}> | ||
| 228 | <section title="News" className='homepage-panel'> | ||
| 229 | <div id='newsContent' style={{ display: "block", width: "100%", overflowY: "scroll", height: "calc(100% - 50px)" }}> | ||
| 230 | {newsList.map((newsList, index) => ( | ||
| 231 | <News newsInfo={newsList} key={index}></News> | ||
| 232 | ))} | ||
| 233 | </div> | ||
| 234 | </section> | ||
| 235 | </div> | ||
| 236 | </div> | ||
| 237 | |||
| 238 | |||
| 239 | |||
| 240 | </main> | ||
| 241 | ) | ||
| 242 | } \ No newline at end of file | ||
diff --git a/frontend/src/components/pages/maplist.css b/frontend/src/components/pages/maplist.css deleted file mode 100644 index b56aacc..0000000 --- a/frontend/src/components/pages/maplist.css +++ /dev/null | |||
| @@ -1,403 +0,0 @@ | |||
| 1 | .maplist-page { | ||
| 2 | position: relative; | ||
| 3 | left: 350px; | ||
| 4 | height: 100vh; | ||
| 5 | color: #cdcfdf; | ||
| 6 | width: calc(100% - 380px); | ||
| 7 | font-family: BarlowSemiCondensed-Regular; | ||
| 8 | overflow-y: scroll; | ||
| 9 | overflow-x: hidden; | ||
| 10 | padding-right: 30px; | ||
| 11 | } | ||
| 12 | |||
| 13 | a { | ||
| 14 | color: inherit; | ||
| 15 | width: fit-content; | ||
| 16 | } | ||
| 17 | |||
| 18 | .maplist-page-content { | ||
| 19 | position: absolute; | ||
| 20 | left: 0px; | ||
| 21 | width: calc(100% - 50px); | ||
| 22 | } | ||
| 23 | |||
| 24 | .maplist-page-header { | ||
| 25 | margin-top: 33px; | ||
| 26 | display: grid; | ||
| 27 | margin-bottom: 10px; | ||
| 28 | } | ||
| 29 | |||
| 30 | .nav-btn { | ||
| 31 | height: 40px; | ||
| 32 | background-color: #2b2e46; | ||
| 33 | color: inherit; | ||
| 34 | font-size: 18px; | ||
| 35 | font-family: inherit; | ||
| 36 | border: none; | ||
| 37 | border-radius: 20px; | ||
| 38 | transition: background-color .1s; | ||
| 39 | cursor: default; | ||
| 40 | width: fit-content; | ||
| 41 | } | ||
| 42 | |||
| 43 | .nav-btn>span { | ||
| 44 | padding: 0 8px 0 8px; | ||
| 45 | } | ||
| 46 | |||
| 47 | .nav-btn:hover { | ||
| 48 | background-color: #202232; | ||
| 49 | cursor: pointer; | ||
| 50 | } | ||
| 51 | |||
| 52 | .game { | ||
| 53 | width: 100%; | ||
| 54 | height: 192px; | ||
| 55 | /* background: #202232; */ | ||
| 56 | border-radius: 24px; | ||
| 57 | overflow: hidden; | ||
| 58 | } | ||
| 59 | |||
| 60 | .game-header { | ||
| 61 | width: 100%; | ||
| 62 | height: 144px; | ||
| 63 | display: flex; | ||
| 64 | justify-content: center; | ||
| 65 | align-items: center; | ||
| 66 | overflow: hidden; | ||
| 67 | } | ||
| 68 | |||
| 69 | .game-header-text { | ||
| 70 | display: flex; | ||
| 71 | justify-content: center; | ||
| 72 | align-items: center; | ||
| 73 | position: absolute; | ||
| 74 | } | ||
| 75 | |||
| 76 | .game-img { | ||
| 77 | width: 100%; | ||
| 78 | height: 100%; | ||
| 79 | background-size: cover; | ||
| 80 | filter: blur(4px); | ||
| 81 | } | ||
| 82 | |||
| 83 | .game-header-text>span { | ||
| 84 | font-size: 42px; | ||
| 85 | font-weight: 500; | ||
| 86 | margin: 5px; | ||
| 87 | } | ||
| 88 | |||
| 89 | .game-header-text span>b { | ||
| 90 | font-size: 96px; | ||
| 91 | font-weight: 600; | ||
| 92 | } | ||
| 93 | |||
| 94 | .game-nav { | ||
| 95 | display: flex; | ||
| 96 | height: 48px; | ||
| 97 | } | ||
| 98 | |||
| 99 | .game-nav-btn { | ||
| 100 | width: 100%; | ||
| 101 | height: 100%; | ||
| 102 | border: none; | ||
| 103 | border-radius: 0px; | ||
| 104 | color: inherit; | ||
| 105 | font-family: inherit; | ||
| 106 | font-size: 22px; | ||
| 107 | background: #2B2E46; | ||
| 108 | transition: background-color .1s; | ||
| 109 | margin: 0 1px; | ||
| 110 | display: flex; | ||
| 111 | justify-content: center; | ||
| 112 | align-items: center; | ||
| 113 | } | ||
| 114 | |||
| 115 | .game-nav-btn:hover { | ||
| 116 | cursor: pointer; | ||
| 117 | } | ||
| 118 | |||
| 119 | .selected { | ||
| 120 | background-color: #202232; | ||
| 121 | } | ||
| 122 | |||
| 123 | .gameview-nav { | ||
| 124 | margin-top: 20px; | ||
| 125 | display: flex; | ||
| 126 | height: 56px; | ||
| 127 | border-radius: 24px; | ||
| 128 | overflow: hidden; | ||
| 129 | gap: 0.06em; | ||
| 130 | /* background-color: #202232; */ | ||
| 131 | } | ||
| 132 | |||
| 133 | .maplist { | ||
| 134 | width: 100%; | ||
| 135 | margin-top: 20px; | ||
| 136 | margin-bottom: 40px; | ||
| 137 | } | ||
| 138 | |||
| 139 | .chapter-name { | ||
| 140 | font-size: 30px; | ||
| 141 | } | ||
| 142 | |||
| 143 | .chapter-page-div { | ||
| 144 | display: flex; | ||
| 145 | justify-content: right; | ||
| 146 | transform: translateY(-30px); | ||
| 147 | } | ||
| 148 | |||
| 149 | .chapter-page-div button { | ||
| 150 | background-color: #00000000; | ||
| 151 | border: 0; | ||
| 152 | cursor: pointer; | ||
| 153 | height: 30px; | ||
| 154 | padding: 0; | ||
| 155 | width: 30px; | ||
| 156 | } | ||
| 157 | |||
| 158 | .chapter-page-div span { | ||
| 159 | color: #cdcfdf; | ||
| 160 | font-family: BarlowSemiCondensed-Regular; | ||
| 161 | font-size: 20px; | ||
| 162 | } | ||
| 163 | |||
| 164 | .maplist-maps { | ||
| 165 | display: grid; | ||
| 166 | grid-template-columns: 25% 25% 25% 25%; | ||
| 167 | margin-top: 10px; | ||
| 168 | transform: translateY(-30px); | ||
| 169 | } | ||
| 170 | |||
| 171 | .maplist-item { | ||
| 172 | background: #202232; | ||
| 173 | border-radius: 24px; | ||
| 174 | overflow: hidden; | ||
| 175 | margin: 10px 10px; | ||
| 176 | /* padding: 10px 15px; */ | ||
| 177 | cursor: pointer; | ||
| 178 | user-select: none; | ||
| 179 | } | ||
| 180 | |||
| 181 | .loader-map { | ||
| 182 | border-radius: 24px; | ||
| 183 | overflow: hidden; | ||
| 184 | margin: 10px 10px; | ||
| 185 | /* padding: 10px 15px; */ | ||
| 186 | user-select: none; | ||
| 187 | width: calc(100% - 20px); | ||
| 188 | height: calc(223px); | ||
| 189 | } | ||
| 190 | |||
| 191 | .maplist-img-div { | ||
| 192 | height: 150px; | ||
| 193 | overflow: hidden; | ||
| 194 | } | ||
| 195 | |||
| 196 | .maplist-img { | ||
| 197 | width: 100%; | ||
| 198 | height: 100%; | ||
| 199 | background-size: cover; | ||
| 200 | filter: blur(4px); | ||
| 201 | opacity: 0.7; | ||
| 202 | } | ||
| 203 | |||
| 204 | .maplist-portalcount-div { | ||
| 205 | display: flex; | ||
| 206 | justify-content: center; | ||
| 207 | align-items: center; | ||
| 208 | text-align: center; | ||
| 209 | height: 100%; | ||
| 210 | transform: translateY(-100%); | ||
| 211 | overflow: hidden; | ||
| 212 | } | ||
| 213 | |||
| 214 | .maplist-title { | ||
| 215 | font-size: 22px; | ||
| 216 | text-align: center; | ||
| 217 | width: 100%; | ||
| 218 | display: inherit; | ||
| 219 | padding: 5px 0px; | ||
| 220 | color: #CDCFDF; | ||
| 221 | } | ||
| 222 | |||
| 223 | .maplist-portals { | ||
| 224 | margin-left: 5px; | ||
| 225 | font-size: 32px; | ||
| 226 | } | ||
| 227 | |||
| 228 | .difficulty-div { | ||
| 229 | display: flex; | ||
| 230 | padding: 7px 10px; | ||
| 231 | } | ||
| 232 | |||
| 233 | .difficulty-label { | ||
| 234 | font-size: 18px; | ||
| 235 | } | ||
| 236 | |||
| 237 | .difficulty-bar { | ||
| 238 | width: 100%; | ||
| 239 | display: grid; | ||
| 240 | grid-template-columns: 20% 20% 20% 20% 20%; | ||
| 241 | align-items: center; | ||
| 242 | margin: 5px; | ||
| 243 | } | ||
| 244 | |||
| 245 | .difficulty-point { | ||
| 246 | background: #2B2E46; | ||
| 247 | height: 3px; | ||
| 248 | margin: 5px; | ||
| 249 | border-radius: 10px; | ||
| 250 | } | ||
| 251 | |||
| 252 | .stats { | ||
| 253 | margin-top: 30px; | ||
| 254 | } | ||
| 255 | |||
| 256 | .portalcount-over-time-div { | ||
| 257 | width: 100%; | ||
| 258 | height: 450px; | ||
| 259 | position: relative; | ||
| 260 | background-color: #202232; | ||
| 261 | border-radius: 20px; | ||
| 262 | } | ||
| 263 | |||
| 264 | .graph-title { | ||
| 265 | width: 100%; | ||
| 266 | display: inherit; | ||
| 267 | font-size: 24px; | ||
| 268 | margin-top: 5px; | ||
| 269 | text-align: center; | ||
| 270 | font-family: BarlowSemiCondensed-SemiBold; | ||
| 271 | padding-top: 7px; | ||
| 272 | } | ||
| 273 | |||
| 274 | .portalcount-graph { | ||
| 275 | height: calc(100% - 30px); | ||
| 276 | width: calc(100% - 80px); | ||
| 277 | } | ||
| 278 | |||
| 279 | .chart { | ||
| 280 | height: calc(100% - 80px); | ||
| 281 | width: 100%; | ||
| 282 | position: relative; | ||
| 283 | padding: 0px 0px; | ||
| 284 | scrollbar-width: thin; | ||
| 285 | } | ||
| 286 | |||
| 287 | .line-chart { | ||
| 288 | list-style: none; | ||
| 289 | margin: 0; | ||
| 290 | padding: 0; | ||
| 291 | height: 100%; | ||
| 292 | border-bottom: 2px solid #2B2E46; | ||
| 293 | } | ||
| 294 | |||
| 295 | .data-point { | ||
| 296 | background-color: #202232; | ||
| 297 | border: 4px solid #006FDE; | ||
| 298 | border-radius: 50%; | ||
| 299 | height: 6px; | ||
| 300 | position: absolute; | ||
| 301 | width: 6px; | ||
| 302 | bottom: calc(var(--y) - 4.5px); | ||
| 303 | left: calc(var(--x) - 6.5px); | ||
| 304 | transition: all 0.2s cubic-bezier(0.075, 0.82, 0.165, 1); | ||
| 305 | z-index: 1; | ||
| 306 | animation: point_intro 0.2s cubic-bezier(0.075, 0.82, 0.165, 1.8); | ||
| 307 | animation-fill-mode: backwards; | ||
| 308 | } | ||
| 309 | |||
| 310 | .data-point:hover, .data-point-active { | ||
| 311 | background-color: #006FDE; | ||
| 312 | box-shadow: 0px 0px 10px #006FDE; | ||
| 313 | } | ||
| 314 | |||
| 315 | .line-segment { | ||
| 316 | background-color: #006FDE; | ||
| 317 | bottom: var(--y); | ||
| 318 | height: 4px; | ||
| 319 | left: var(--x); | ||
| 320 | position: absolute; | ||
| 321 | transform: rotate(calc(var(--angle) * -1deg)); | ||
| 322 | width: calc(var(--hypotenuse) * 1px); | ||
| 323 | transform-origin: left bottom; | ||
| 324 | border-radius: 20px; | ||
| 325 | z-index: 1; | ||
| 326 | animation: line_intro 0.05s cubic-bezier(0, 1, 0.31, 0.96); | ||
| 327 | animation-fill-mode: backwards; | ||
| 328 | } | ||
| 329 | |||
| 330 | #dataPointInfo { | ||
| 331 | position: absolute; | ||
| 332 | width: 400px; | ||
| 333 | height: 85px; | ||
| 334 | background: #202232; | ||
| 335 | box-shadow: 0px 4px 16px 0px #00000080; | ||
| 336 | transition: all 0.3s cubic-bezier(0.075, 0.82, 0.165, 1); | ||
| 337 | z-index: 1000; | ||
| 338 | opacity: 0; | ||
| 339 | left: auto; | ||
| 340 | border-radius: 20px; | ||
| 341 | padding: 15px 7px; | ||
| 342 | } | ||
| 343 | |||
| 344 | .section-header { | ||
| 345 | display: flex; | ||
| 346 | text-align: center; | ||
| 347 | font-family: BarlowSemiCondensed-SemiBold; | ||
| 348 | font-size: 18px; | ||
| 349 | height: 40%; | ||
| 350 | justify-content: space-evenly; | ||
| 351 | align-items: center; | ||
| 352 | } | ||
| 353 | |||
| 354 | .section-header span, .section-data span { | ||
| 355 | flex: 1; | ||
| 356 | } | ||
| 357 | |||
| 358 | .divider { | ||
| 359 | width: 100%; | ||
| 360 | height: 2px; | ||
| 361 | background-color: #2B2E46; | ||
| 362 | display: flex; | ||
| 363 | margin: 5px 0px 8px 0px; | ||
| 364 | } | ||
| 365 | |||
| 366 | .section-data { | ||
| 367 | display: flex; | ||
| 368 | grid-template-columns: 25% 25% 25% 25%; | ||
| 369 | text-align: center; | ||
| 370 | background-color: #2B2E46; | ||
| 371 | height: 52%; | ||
| 372 | border-radius: 200px; | ||
| 373 | align-items: center; | ||
| 374 | justify-content: space-evenly; | ||
| 375 | flex-grow: 1; | ||
| 376 | font-family: BarlowSemiCondensed-Regular; | ||
| 377 | font-size: 18px; | ||
| 378 | padding: 0px 5px; | ||
| 379 | } | ||
| 380 | |||
| 381 | @keyframes line_intro { | ||
| 382 | 0% { | ||
| 383 | width: 0; | ||
| 384 | } | ||
| 385 | 100% { | ||
| 386 | width: calc(var(--hypotenuse) * 1px); | ||
| 387 | } | ||
| 388 | } | ||
| 389 | |||
| 390 | @keyframes point_intro { | ||
| 391 | 0% { | ||
| 392 | opacity: 0; | ||
| 393 | width: 0; | ||
| 394 | height: 0; | ||
| 395 | transform: translate(3px, -3px); | ||
| 396 | } | ||
| 397 | 100% { | ||
| 398 | width: 6px; | ||
| 399 | height: 6px; | ||
| 400 | transform: translate(0px, 0px); | ||
| 401 | opacity: 1; | ||
| 402 | } | ||
| 403 | } | ||
diff --git a/frontend/src/components/pages/maplist.js b/frontend/src/components/pages/maplist.js deleted file mode 100644 index a5c6c19..0000000 --- a/frontend/src/components/pages/maplist.js +++ /dev/null | |||
| @@ -1,890 +0,0 @@ | |||
| 1 | import React, { useEffect, useRef, useState } from 'react'; | ||
| 2 | import { useLocation, Link } from "react-router-dom"; | ||
| 3 | import { BrowserRouter as Router, Route, Routes, useNavigate } from 'react-router-dom'; | ||
| 4 | |||
| 5 | import "./maplist.css" | ||
| 6 | import img5 from "../../imgs/5.png" | ||
| 7 | import img6 from "../../imgs/6.png" | ||
| 8 | |||
| 9 | export default function Maplist(prop) { | ||
| 10 | const { token, setToken } = prop | ||
| 11 | const scrollRef = useRef(null) | ||
| 12 | const [games, setGames] = React.useState(null); | ||
| 13 | const [hasOpenedStatistics, setHasOpenedStatistics] = React.useState(false); | ||
| 14 | const [totalPortals, setTotalPortals] = React.useState(0); | ||
| 15 | const [loading, setLoading] = React.useState(true) | ||
| 16 | const location = useLocation(); | ||
| 17 | |||
| 18 | const [gameTitle, setGameTitle] = React.useState(""); | ||
| 19 | const [catPortalCount, setCatPortalCount] = React.useState(0); | ||
| 20 | let minPage; | ||
| 21 | let maxPage; | ||
| 22 | let currentPage; | ||
| 23 | let add = 0; | ||
| 24 | let gameState; | ||
| 25 | let catState = 0; | ||
| 26 | async function detectGame() { | ||
| 27 | const response = await fetch("https://lp.ardapektezol.com/api/v1/games", { | ||
| 28 | headers: { | ||
| 29 | 'Authorization': token | ||
| 30 | } | ||
| 31 | }); | ||
| 32 | |||
| 33 | const data = await response.json(); | ||
| 34 | |||
| 35 | const url = new URL(window.location.href) | ||
| 36 | |||
| 37 | const params = new URLSearchParams(url.search) | ||
| 38 | gameState = parseFloat(location.pathname.split("/")[2]) | ||
| 39 | |||
| 40 | if (gameState == 1) { | ||
| 41 | setGameTitle(data.data[0].name); | ||
| 42 | |||
| 43 | maxPage = 9; | ||
| 44 | minPage = 1; | ||
| 45 | createCategories(1); | ||
| 46 | } else if (gameState == 2) { | ||
| 47 | setGameTitle(data.data[1].name); | ||
| 48 | |||
| 49 | maxPage = 16; | ||
| 50 | minPage = 10; | ||
| 51 | add = 10 | ||
| 52 | createCategories(2); | ||
| 53 | } | ||
| 54 | |||
| 55 | let chapterParam = params.get("chapter") | ||
| 56 | |||
| 57 | currentPage = minPage; | ||
| 58 | |||
| 59 | if (chapterParam) { | ||
| 60 | currentPage = +chapterParam + add | ||
| 61 | } | ||
| 62 | |||
| 63 | changePage(currentPage); | ||
| 64 | |||
| 65 | // if (!loading) { | ||
| 66 | |||
| 67 | // document.querySelector("#catPortalCount").innerText = data.data[gameState - 1].category_portals[0].portal_count; | ||
| 68 | |||
| 69 | // } | ||
| 70 | |||
| 71 | setCatPortalCount(data.data[gameState - 1].category_portals[0].portal_count); | ||
| 72 | |||
| 73 | // if (chapterParam) { | ||
| 74 | // document.querySelector("#pageNumbers").innerText = `${chapterParam - minPage + 1}/${maxPage - minPage + 1}` | ||
| 75 | // } | ||
| 76 | } | ||
| 77 | |||
| 78 | function changeMaplistOrStatistics(index, name) { | ||
| 79 | const maplistBtns = document.querySelectorAll("#maplistBtn"); | ||
| 80 | maplistBtns.forEach((btn, i) => { | ||
| 81 | if (i == index) { | ||
| 82 | btn.className = "game-nav-btn selected" | ||
| 83 | |||
| 84 | if (name == "maplist") { | ||
| 85 | document.querySelector(".stats").style.display = "none"; | ||
| 86 | document.querySelector(".maplist").style.display = "block"; | ||
| 87 | document.querySelector(".maplist").setAttribute("currentTab", "maplist"); | ||
| 88 | } else { | ||
| 89 | document.querySelector(".stats").style.display = "block"; | ||
| 90 | document.querySelector(".maplist").style.display = "none"; | ||
| 91 | |||
| 92 | document.querySelector(".maplist-page").scrollTo({ top: 372, behavior: "smooth" }) | ||
| 93 | document.querySelector(".maplist").setAttribute("currentTab", "stats"); | ||
| 94 | setHasOpenedStatistics(true); | ||
| 95 | } | ||
| 96 | } else { | ||
| 97 | btn.className = "game-nav-btn"; | ||
| 98 | } | ||
| 99 | }); | ||
| 100 | } | ||
| 101 | |||
| 102 | async function createCategories(gameID) { | ||
| 103 | const response = await fetch("https://lp.ardapektezol.com/api/v1/games", { | ||
| 104 | headers: { | ||
| 105 | 'Authorization': token | ||
| 106 | } | ||
| 107 | }); | ||
| 108 | |||
| 109 | const data = await response.json(); | ||
| 110 | let categoriesArr = data.data[gameID - 1].category_portals; | ||
| 111 | |||
| 112 | if (document.querySelector(".maplist-maps") == null) { | ||
| 113 | return; | ||
| 114 | } | ||
| 115 | const gameNav = document.querySelector(".game-nav"); | ||
| 116 | gameNav.innerHTML = ""; | ||
| 117 | categoriesArr.forEach((category) => { | ||
| 118 | createCategory(category); | ||
| 119 | }); | ||
| 120 | |||
| 121 | setLoading(false); | ||
| 122 | } | ||
| 123 | |||
| 124 | let categoryNum = 0; | ||
| 125 | function createCategory(category) { | ||
| 126 | const gameNav = document.querySelector(".game-nav"); | ||
| 127 | |||
| 128 | categoryNum++; | ||
| 129 | const gameNavBtn = document.createElement("button"); | ||
| 130 | if (categoryNum == 1) { | ||
| 131 | gameNavBtn.className = "game-nav-btn selected"; | ||
| 132 | } else { | ||
| 133 | gameNavBtn.className = "game-nav-btn"; | ||
| 134 | } | ||
| 135 | gameNavBtn.id = "catBtn" | ||
| 136 | gameNavBtn.innerText = category.category.name; | ||
| 137 | |||
| 138 | gameNavBtn.addEventListener("click", (e) => { | ||
| 139 | changeCategory(category, e); | ||
| 140 | changePage(currentPage); | ||
| 141 | }) | ||
| 142 | |||
| 143 | gameNav.appendChild(gameNavBtn); | ||
| 144 | } | ||
| 145 | |||
| 146 | async function changeCategory(category, btn) { | ||
| 147 | const navBtns = document.querySelectorAll("#catBtn"); | ||
| 148 | navBtns.forEach((btns) => { | ||
| 149 | btns.classList.remove("selected"); | ||
| 150 | }); | ||
| 151 | |||
| 152 | btn.srcElement.classList.add("selected"); | ||
| 153 | const response = await fetch("https://lp.ardapektezol.com/api/v1/games", { | ||
| 154 | headers: { | ||
| 155 | 'Authorization': token | ||
| 156 | } | ||
| 157 | }); | ||
| 158 | |||
| 159 | const data = await response.json(); | ||
| 160 | catState = category.category.id - 1; | ||
| 161 | // console.log(catState) | ||
| 162 | document.querySelector("#catPortalCount").innerText = category.portal_count; | ||
| 163 | } | ||
| 164 | |||
| 165 | async function changePage(page) { | ||
| 166 | const pageNumbers = document.querySelector("#pageNumbers"); | ||
| 167 | |||
| 168 | pageNumbers.innerText = `${currentPage - minPage + 1}/${maxPage - minPage + 1}`; | ||
| 169 | |||
| 170 | const maplistMaps = document.querySelector(".maplist-maps"); | ||
| 171 | maplistMaps.innerHTML = ""; | ||
| 172 | for (let index = 0; index < 8; index++) { | ||
| 173 | const loadingAnimation = document.createElement("div"); | ||
| 174 | loadingAnimation.classList.add("loader"); | ||
| 175 | loadingAnimation.classList.add("loader-map") | ||
| 176 | maplistMaps.appendChild(loadingAnimation); | ||
| 177 | } | ||
| 178 | const data = await fetchMaps(page); | ||
| 179 | const maps = data.data.maps; | ||
| 180 | const name = data.data.chapter.name; | ||
| 181 | |||
| 182 | let chapterName = "Chapter"; | ||
| 183 | const chapterNumberOld = name.split(" - ")[0]; | ||
| 184 | let chapterNumber1 = chapterNumberOld.split("Chapter ")[1]; | ||
| 185 | if (chapterNumber1 == undefined) { | ||
| 186 | chapterName = "Course" | ||
| 187 | chapterNumber1 = chapterNumberOld.split("Course ")[1]; | ||
| 188 | } | ||
| 189 | const chapterNumber = chapterNumber1.toString().padStart(2, "0"); | ||
| 190 | const chapterTitle = name.split(" - ")[1]; | ||
| 191 | |||
| 192 | if (document.querySelector(".maplist-maps") == null) { | ||
| 193 | return; | ||
| 194 | } | ||
| 195 | const chapterNumberElement = document.querySelector(".chapter-num") | ||
| 196 | const chapterTitleElement = document.querySelector(".chapter-name") | ||
| 197 | chapterNumberElement.innerText = chapterName + " " + chapterNumber; | ||
| 198 | chapterTitleElement.innerText = chapterTitle; | ||
| 199 | |||
| 200 | maplistMaps.innerHTML = ""; | ||
| 201 | maps.forEach(map => { | ||
| 202 | let portalCount; | ||
| 203 | if (map.category_portals[catState] != undefined) { | ||
| 204 | portalCount = map.category_portals[catState].portal_count; | ||
| 205 | } else { | ||
| 206 | portalCount = map.category_portals[0].portal_count; | ||
| 207 | } | ||
| 208 | addMap(map.name, portalCount, map.image, map.difficulty + 1, map.id); | ||
| 209 | }); | ||
| 210 | |||
| 211 | const url = new URL(window.location.href) | ||
| 212 | |||
| 213 | const params = new URLSearchParams(url.search) | ||
| 214 | |||
| 215 | let chapterParam = params.get("chapter") | ||
| 216 | |||
| 217 | try { | ||
| 218 | const response = await fetch("https://lp.ardapektezol.com/api/v1/games", { | ||
| 219 | headers: { | ||
| 220 | 'Authorization': token | ||
| 221 | } | ||
| 222 | }); | ||
| 223 | |||
| 224 | const data = await response.json(); | ||
| 225 | |||
| 226 | const gameImg = document.querySelector(".game-img"); | ||
| 227 | |||
| 228 | gameImg.style.backgroundImage = `url(${data.data[0].image})`; | ||
| 229 | |||
| 230 | // const mapImg = document.querySelectorAll(".maplist-img"); | ||
| 231 | // mapImg.forEach((map) => { | ||
| 232 | // map.style.backgroundImage = `url(${data.data[0].image})`; | ||
| 233 | // }); | ||
| 234 | |||
| 235 | } catch (error) { | ||
| 236 | console.log("error fetching games:", error); | ||
| 237 | } | ||
| 238 | |||
| 239 | asignDifficulties(); | ||
| 240 | } | ||
| 241 | |||
| 242 | async function addMap(mapName, mapPortalCount, mapImage, difficulty, mapID) { | ||
| 243 | // jesus christ | ||
| 244 | const maplistItem = document.createElement("div"); | ||
| 245 | const maplistTitle = document.createElement("span"); | ||
| 246 | const maplistImgDiv = document.createElement("div"); | ||
| 247 | const maplistImg = document.createElement("div"); | ||
| 248 | const maplistPortalcountDiv = document.createElement("div"); | ||
| 249 | const maplistPortalcount = document.createElement("span"); | ||
| 250 | const b = document.createElement("b"); | ||
| 251 | const maplistPortalcountPortals = document.createElement("span"); | ||
| 252 | const difficultyDiv = document.createElement("div"); | ||
| 253 | const difficultyLabel = document.createElement("span"); | ||
| 254 | const difficultyBar = document.createElement("div"); | ||
| 255 | const difficultyPoint1 = document.createElement("div"); | ||
| 256 | const difficultyPoint2 = document.createElement("div"); | ||
| 257 | const difficultyPoint3 = document.createElement("div"); | ||
| 258 | const difficultyPoint4 = document.createElement("div"); | ||
| 259 | const difficultyPoint5 = document.createElement("div"); | ||
| 260 | |||
| 261 | maplistItem.className = "maplist-item"; | ||
| 262 | maplistTitle.className = "maplist-title"; | ||
| 263 | maplistImgDiv.className = "maplist-img-div"; | ||
| 264 | maplistImg.className = "maplist-img"; | ||
| 265 | maplistPortalcountDiv.className = "maplist-portalcount-div"; | ||
| 266 | maplistPortalcount.className = "maplist-portalcount"; | ||
| 267 | maplistPortalcountPortals.className = "maplist-portals"; | ||
| 268 | difficultyDiv.className = "difficulty-div"; | ||
| 269 | difficultyLabel.className = "difficulty-label"; | ||
| 270 | difficultyBar.className = "difficulty-bar"; | ||
| 271 | difficultyPoint1.className = "difficulty-point"; | ||
| 272 | difficultyPoint2.className = "difficulty-point"; | ||
| 273 | difficultyPoint3.className = "difficulty-point"; | ||
| 274 | difficultyPoint4.className = "difficulty-point"; | ||
| 275 | difficultyPoint5.className = "difficulty-point"; | ||
| 276 | |||
| 277 | |||
| 278 | maplistTitle.innerText = mapName; | ||
| 279 | difficultyLabel.innerText = "Difficulty: " | ||
| 280 | maplistPortalcountPortals.innerText = "portals" | ||
| 281 | b.innerText = mapPortalCount; | ||
| 282 | maplistImg.style.backgroundImage = `url(${mapImage})`; | ||
| 283 | difficultyBar.setAttribute("difficulty", difficulty) | ||
| 284 | maplistItem.setAttribute("id", mapID) | ||
| 285 | maplistItem.addEventListener("click", () => { | ||
| 286 | console.log(mapID) | ||
| 287 | window.location.href = "/maps/" + mapID | ||
| 288 | }) | ||
| 289 | |||
| 290 | // appends | ||
| 291 | // maplist item | ||
| 292 | maplistItem.appendChild(maplistTitle); | ||
| 293 | maplistImgDiv.appendChild(maplistImg); | ||
| 294 | maplistImgDiv.appendChild(maplistPortalcountDiv); | ||
| 295 | maplistPortalcountDiv.appendChild(maplistPortalcount); | ||
| 296 | maplistPortalcount.appendChild(b); | ||
| 297 | maplistPortalcountDiv.appendChild(maplistPortalcountPortals); | ||
| 298 | maplistItem.appendChild(maplistImgDiv); | ||
| 299 | maplistItem.appendChild(difficultyDiv); | ||
| 300 | difficultyDiv.appendChild(difficultyLabel); | ||
| 301 | difficultyDiv.appendChild(difficultyBar); | ||
| 302 | difficultyBar.appendChild(difficultyPoint1); | ||
| 303 | difficultyBar.appendChild(difficultyPoint2); | ||
| 304 | difficultyBar.appendChild(difficultyPoint3); | ||
| 305 | difficultyBar.appendChild(difficultyPoint4); | ||
| 306 | difficultyBar.appendChild(difficultyPoint5); | ||
| 307 | |||
| 308 | // display in place | ||
| 309 | const maplistMaps = document.querySelector(".maplist-maps"); | ||
| 310 | maplistMaps.appendChild(maplistItem); | ||
| 311 | } | ||
| 312 | |||
| 313 | async function fetchMaps(chapterID) { | ||
| 314 | try { | ||
| 315 | const response = await fetch(`https://lp.ardapektezol.com/api/v1/chapters/${chapterID}`, { | ||
| 316 | headers: { | ||
| 317 | 'Authorization': token | ||
| 318 | } | ||
| 319 | }); | ||
| 320 | |||
| 321 | const data = await response.json(); | ||
| 322 | return data; | ||
| 323 | } catch (err) { | ||
| 324 | console.log(err) | ||
| 325 | } | ||
| 326 | } | ||
| 327 | |||
| 328 | // difficulty stuff | ||
| 329 | function asignDifficulties() { | ||
| 330 | const difficulties = document.querySelectorAll(".difficulty-bar"); | ||
| 331 | difficulties.forEach((difficultyElement) => { | ||
| 332 | let difficulty = difficultyElement.getAttribute("difficulty"); | ||
| 333 | if (difficulty == "1") { | ||
| 334 | difficultyElement.childNodes[0].style.backgroundColor = "#51C355"; | ||
| 335 | } else if (difficulty == "2") { | ||
| 336 | difficultyElement.childNodes[0].style.backgroundColor = "#8AC93A"; | ||
| 337 | difficultyElement.childNodes[1].style.backgroundColor = "#8AC93A"; | ||
| 338 | } else if (difficulty == "3") { | ||
| 339 | difficultyElement.childNodes[0].style.backgroundColor = "#8AC93A"; | ||
| 340 | difficultyElement.childNodes[1].style.backgroundColor = "#8AC93A"; | ||
| 341 | difficultyElement.childNodes[2].style.backgroundColor = "#8AC93A"; | ||
| 342 | } else if (difficulty == "4") { | ||
| 343 | difficultyElement.childNodes[0].style.backgroundColor = "#C35F51"; | ||
| 344 | difficultyElement.childNodes[1].style.backgroundColor = "#C35F51"; | ||
| 345 | difficultyElement.childNodes[2].style.backgroundColor = "#C35F51"; | ||
| 346 | difficultyElement.childNodes[3].style.backgroundColor = "#C35F51"; | ||
| 347 | } else if (difficulty == "5") { | ||
| 348 | difficultyElement.childNodes[0].style.backgroundColor = "#C35F51"; | ||
| 349 | difficultyElement.childNodes[1].style.backgroundColor = "#C35F51"; | ||
| 350 | difficultyElement.childNodes[2].style.backgroundColor = "#C35F51"; | ||
| 351 | difficultyElement.childNodes[3].style.backgroundColor = "#C35F51"; | ||
| 352 | difficultyElement.childNodes[4].style.backgroundColor = "#C35F51"; | ||
| 353 | } | ||
| 354 | }); | ||
| 355 | } | ||
| 356 | |||
| 357 | const divRef = useRef(null); | ||
| 358 | |||
| 359 | React.useEffect(() => { | ||
| 360 | |||
| 361 | const lineChart = document.querySelector(".line-chart") | ||
| 362 | let tempTotalPortals = 0 | ||
| 363 | fetch("https://lp.ardapektezol.com/api/v1/games/1/maps", { | ||
| 364 | headers: { | ||
| 365 | 'Authorization': token | ||
| 366 | } | ||
| 367 | }) | ||
| 368 | .then(r => r.json()) | ||
| 369 | .then(d => { | ||
| 370 | d.data.maps.forEach((map, i) => { | ||
| 371 | tempTotalPortals += map.portal_count | ||
| 372 | }) | ||
| 373 | }) | ||
| 374 | .then(() => { | ||
| 375 | setTotalPortals(tempTotalPortals) | ||
| 376 | }) | ||
| 377 | async function createGraph() { | ||
| 378 | console.log(totalPortals) | ||
| 379 | // max | ||
| 380 | let items = [ | ||
| 381 | { | ||
| 382 | record: "100", | ||
| 383 | date: new Date(2011, 4, 4), | ||
| 384 | map: "Container Ride", | ||
| 385 | first: "tiny zach" | ||
| 386 | }, | ||
| 387 | { | ||
| 388 | record: "98", | ||
| 389 | date: new Date(2012, 6, 4), | ||
| 390 | map: "Container Ride", | ||
| 391 | first: "tiny zach" | ||
| 392 | }, | ||
| 393 | { | ||
| 394 | record: "94", | ||
| 395 | date: new Date(2013, 0, 1), | ||
| 396 | map: "Container Ride", | ||
| 397 | first: "tiny zach" | ||
| 398 | }, | ||
| 399 | { | ||
| 400 | record: "90", | ||
| 401 | date: new Date(2014, 0, 1), | ||
| 402 | map: "Container Ride", | ||
| 403 | first: "tiny zach" | ||
| 404 | }, | ||
| 405 | { | ||
| 406 | record: "88", | ||
| 407 | date: new Date(2015, 6, 14), | ||
| 408 | map: "Container Ride", | ||
| 409 | first: "tiny zach" | ||
| 410 | }, | ||
| 411 | { | ||
| 412 | record: "84", | ||
| 413 | date: new Date(2016, 8, 19), | ||
| 414 | map: "Container Ride", | ||
| 415 | first: "tiny zach" | ||
| 416 | }, | ||
| 417 | { | ||
| 418 | record: "82", | ||
| 419 | date: new Date(2017, 3, 20), | ||
| 420 | map: "Container Ride", | ||
| 421 | first: "tiny zach" | ||
| 422 | }, | ||
| 423 | { | ||
| 424 | record: "81", | ||
| 425 | date: new Date(2018, 2, 25), | ||
| 426 | map: "Container Ride", | ||
| 427 | first: "tiny zach" | ||
| 428 | }, | ||
| 429 | { | ||
| 430 | record: "80", | ||
| 431 | date: new Date(2019, 3, 4), | ||
| 432 | map: "Container Ride", | ||
| 433 | first: "tiny zach" | ||
| 434 | }, | ||
| 435 | { | ||
| 436 | record: "78", | ||
| 437 | date: new Date(2020, 11, 21), | ||
| 438 | map: "Container Ride", | ||
| 439 | first: "tiny zach" | ||
| 440 | }, | ||
| 441 | { | ||
| 442 | record: "77", | ||
| 443 | date: new Date(2021, 10, 25), | ||
| 444 | map: "Container Ride", | ||
| 445 | first: "tiny zach" | ||
| 446 | }, | ||
| 447 | { | ||
| 448 | record: "76", | ||
| 449 | date: new Date(2022, 4, 17), | ||
| 450 | map: "Container Ride", | ||
| 451 | first: "tiny zach" | ||
| 452 | }, | ||
| 453 | { | ||
| 454 | record: "75", | ||
| 455 | date: new Date(2023, 9, 31), | ||
| 456 | map: "Container Ride", | ||
| 457 | first: "tiny zach" | ||
| 458 | }, | ||
| 459 | { | ||
| 460 | record: "74", | ||
| 461 | date: new Date(2024, 4, 4), | ||
| 462 | map: "Container Ride", | ||
| 463 | first: "tiny zach" | ||
| 464 | }, | ||
| 465 | ] | ||
| 466 | |||
| 467 | function calculatePosition(date, startDate, endDate, maxWidth) { | ||
| 468 | const totalMilliseconds = endDate - startDate + 10000000000; | ||
| 469 | const millisecondsFromStart = date - startDate + 5000000000; | ||
| 470 | return (millisecondsFromStart / totalMilliseconds) * maxWidth | ||
| 471 | } | ||
| 472 | |||
| 473 | const minDate = items.reduce((min, dp) => dp.date < min ? dp.date : min, items[0].date) | ||
| 474 | const maxDate = items.reduce((max, dp) => dp.date > max ? dp.date : max, items[0].date) | ||
| 475 | |||
| 476 | const graph_width = document.querySelector(".portalcount-over-time-div").clientWidth | ||
| 477 | // console.log(graph_width) | ||
| 478 | |||
| 479 | const uniqueYears = new Set() | ||
| 480 | items.forEach(dp => uniqueYears.add(dp.date.getFullYear())) | ||
| 481 | let minYear = Infinity; | ||
| 482 | let maxYear = -Infinity; | ||
| 483 | |||
| 484 | items.forEach(dp => { | ||
| 485 | const year = dp.date.getFullYear(); | ||
| 486 | minYear = Math.min(minYear, year); | ||
| 487 | maxYear = Math.max(maxYear, year); | ||
| 488 | }); | ||
| 489 | |||
| 490 | // Add missing years to the set | ||
| 491 | for (let year = minYear; year <= maxYear; year++) { | ||
| 492 | uniqueYears.add(year); | ||
| 493 | } | ||
| 494 | const uniqueYearsArr = Array.from(uniqueYears) | ||
| 495 | |||
| 496 | items = items.map(dp => ({ | ||
| 497 | record: dp.record, | ||
| 498 | date: dp.date, | ||
| 499 | x: calculatePosition(dp.date, minDate, maxDate, lineChart.clientWidth), | ||
| 500 | map: dp.map, | ||
| 501 | first: dp.first | ||
| 502 | })) | ||
| 503 | |||
| 504 | const yearInterval = lineChart.clientWidth / uniqueYears.size | ||
| 505 | for (let index = 1; index < (uniqueYears.size); index++) { | ||
| 506 | const placeholderlmao = document.createElement("div") | ||
| 507 | const yearSpan = document.createElement("span") | ||
| 508 | yearSpan.style.position = "absolute" | ||
| 509 | placeholderlmao.style.height = "100%" | ||
| 510 | placeholderlmao.style.width = "2px" | ||
| 511 | placeholderlmao.style.backgroundColor = "#00000080" | ||
| 512 | placeholderlmao.style.position = `absolute` | ||
| 513 | const thing = calculatePosition(new Date(uniqueYearsArr[index], 0, 0), minDate, maxDate, lineChart.clientWidth) | ||
| 514 | placeholderlmao.style.left = `${thing}px` | ||
| 515 | yearSpan.style.left = `${thing}px` | ||
| 516 | yearSpan.style.bottom = "-34px" | ||
| 517 | yearSpan.innerText = uniqueYearsArr[index] | ||
| 518 | yearSpan.style.fontFamily = "BarlowSemiCondensed-Regular" | ||
| 519 | yearSpan.style.fontSize = "22px" | ||
| 520 | yearSpan.style.opacity = "0.8" | ||
| 521 | lineChart.appendChild(yearSpan) | ||
| 522 | |||
| 523 | } | ||
| 524 | |||
| 525 | let maxPortals; | ||
| 526 | let minPortals; | ||
| 527 | let precision; | ||
| 528 | let multiplier = 1; | ||
| 529 | for (let index = 0; index < items.length; index++) { | ||
| 530 | precision = Math.floor((items[0].record - items[items.length - 1].record)) | ||
| 531 | if (precision > 20) { | ||
| 532 | precision = 20 | ||
| 533 | } | ||
| 534 | minPortals = Math.floor((items[items.length - 1].record) / 10) * 10 | ||
| 535 | if (index == 0) { | ||
| 536 | maxPortals = items[index].record - minPortals | ||
| 537 | } | ||
| 538 | } | ||
| 539 | function calculateMultiplier(value) { | ||
| 540 | while (value > precision) { | ||
| 541 | multiplier += 1; | ||
| 542 | value -= precision; | ||
| 543 | } | ||
| 544 | } | ||
| 545 | calculateMultiplier(items[0].record); | ||
| 546 | // if (items[0].record > 10) { | ||
| 547 | // multiplier = 2; | ||
| 548 | // } | ||
| 549 | |||
| 550 | // Original cubic bezier control points | ||
| 551 | const P0 = { x: 0, y: 0 }; | ||
| 552 | const P1 = { x: 0.26, y: 1 }; | ||
| 553 | const P2 = { x: 0.74, y: 1 }; | ||
| 554 | const P3 = { x: 1, y: 0 }; | ||
| 555 | |||
| 556 | function calculateIntermediateControlPoints(t, P0, P1, P2, P3) { | ||
| 557 | const x = (1 - t) ** 3 * P0.x + | ||
| 558 | 3 * (1 - t) ** 2 * t * P1.x + | ||
| 559 | 3 * (1 - t) * t ** 2 * P2.x + | ||
| 560 | t ** 3 * P3.x; | ||
| 561 | |||
| 562 | const y = (1 - t) ** 3 * P0.y + | ||
| 563 | 3 * (1 - t) ** 2 * t * P1.y + | ||
| 564 | 3 * (1 - t) * t ** 2 * P2.y + | ||
| 565 | t ** 3 * P3.y; | ||
| 566 | |||
| 567 | return { x, y }; | ||
| 568 | } | ||
| 569 | |||
| 570 | |||
| 571 | let delay = 0; | ||
| 572 | for (let index = 0; index < items.length; index++) { | ||
| 573 | let chart_height = 340; | ||
| 574 | const item = items[index]; | ||
| 575 | delay += 0.05; | ||
| 576 | // console.log(lineChart.clientWidth) | ||
| 577 | |||
| 578 | // maxPortals++; | ||
| 579 | // maxPortals++; | ||
| 580 | |||
| 581 | let point_height = (chart_height / maxPortals) | ||
| 582 | |||
| 583 | for (let index = 0; index < (maxPortals / multiplier); index++) { | ||
| 584 | // console.log((index + 1) * multiplier) | ||
| 585 | let current_portal_count = (index + 1); | ||
| 586 | |||
| 587 | const placeholderDiv = document.createElement("div") | ||
| 588 | const numPortalsText = document.createElement("span") | ||
| 589 | const numPortalsTextBottom = document.createElement("span") | ||
| 590 | numPortalsText.innerText = (current_portal_count * multiplier) + minPortals | ||
| 591 | numPortalsTextBottom.innerText = minPortals | ||
| 592 | placeholderDiv.style.position = "absolute" | ||
| 593 | numPortalsText.style.position = "absolute" | ||
| 594 | numPortalsTextBottom.style.position = "absolute" | ||
| 595 | numPortalsText.style.left = "-37px" | ||
| 596 | numPortalsText.style.opacity = "0.2" | ||
| 597 | numPortalsTextBottom.style.opacity = "0.2" | ||
| 598 | numPortalsText.style.fontFamily = "BarlowSemiCondensed-Regular" | ||
| 599 | numPortalsTextBottom.style.fontFamily = "BarlowSemiCondensed-Regular" | ||
| 600 | numPortalsText.style.fontSize = "22px" | ||
| 601 | numPortalsTextBottom.style.left = "-37px" | ||
| 602 | numPortalsTextBottom.style.fontSize = "22px" | ||
| 603 | numPortalsTextBottom.style.fontWeight = "400" | ||
| 604 | numPortalsText.style.color = "#CDCFDF" | ||
| 605 | numPortalsTextBottom.style.color = "#CDCFDF" | ||
| 606 | numPortalsText.style.fontFamily = "inherit" | ||
| 607 | numPortalsTextBottom.style.fontFamily = "inherit" | ||
| 608 | numPortalsText.style.textAlign = "right" | ||
| 609 | numPortalsTextBottom.style.textAlign = "right" | ||
| 610 | numPortalsText.style.width = "30px" | ||
| 611 | numPortalsTextBottom.style.width = "30px" | ||
| 612 | placeholderDiv.style.bottom = `${(point_height * current_portal_count * multiplier) - 2}px` | ||
| 613 | numPortalsText.style.bottom = `${(point_height * current_portal_count * multiplier) - 2 - 9}px` | ||
| 614 | numPortalsTextBottom.style.bottom = `${0 - 2 - 8}px` | ||
| 615 | placeholderDiv.id = placeholderDiv.style.bottom | ||
| 616 | placeholderDiv.style.width = "100%" | ||
| 617 | placeholderDiv.style.height = "2px" | ||
| 618 | placeholderDiv.style.backgroundColor = "#2B2E46" | ||
| 619 | placeholderDiv.style.zIndex = "0" | ||
| 620 | |||
| 621 | if (index == 0) { | ||
| 622 | lineChart.appendChild(numPortalsTextBottom) | ||
| 623 | } | ||
| 624 | lineChart.appendChild(numPortalsText) | ||
| 625 | lineChart.appendChild(placeholderDiv) | ||
| 626 | } | ||
| 627 | |||
| 628 | const li = document.createElement("li"); | ||
| 629 | const lineSeg = document.createElement("div"); | ||
| 630 | const dataPoint = document.createElement("div"); | ||
| 631 | |||
| 632 | li.style = `--y: ${point_height * (item.record - minPortals) - 3}px; --x: ${item.x}px`; | ||
| 633 | lineSeg.className = "line-segment"; | ||
| 634 | dataPoint.className = "data-point"; | ||
| 635 | |||
| 636 | if (items[index + 1] !== undefined) { | ||
| 637 | const hypotenuse = Math.sqrt( | ||
| 638 | Math.pow(items[index + 1].x - items[index].x, 2) + | ||
| 639 | Math.pow((point_height * items[index + 1].record) - point_height * item.record, 2) | ||
| 640 | ); | ||
| 641 | const angle = Math.asin( | ||
| 642 | ((point_height * item.record) - (point_height * items[index + 1].record)) / hypotenuse | ||
| 643 | ); | ||
| 644 | |||
| 645 | lineSeg.style = `--hypotenuse: ${hypotenuse}; --angle: ${angle * (-180 / Math.PI)}`; | ||
| 646 | const t0 = index / items.length; | ||
| 647 | const t1 = (index + 1) / items.length | ||
| 648 | |||
| 649 | const P0t0 = calculateIntermediateControlPoints(t0, P0, P1, P2, P3); | ||
| 650 | const P1t1 = calculateIntermediateControlPoints(t1, P0, P1, P2, P3); | ||
| 651 | const bezierStyle = `cubic-bezier(${P0t0.x.toFixed(3)}, ${P0t0.y.toFixed(3)}, ${P1t1.x.toFixed(3)}, ${P1t1.y.toFixed(3)})` | ||
| 652 | lineSeg.style.animationTimingFunction = bezierStyle | ||
| 653 | lineSeg.style.animationDelay = delay + "s" | ||
| 654 | } | ||
| 655 | dataPoint.style.animationDelay = delay + "s" | ||
| 656 | |||
| 657 | let isHoveringOverData = true; | ||
| 658 | let isDataActive = false; | ||
| 659 | document.querySelector("#dataPointInfo").style.left = item.x + "px"; | ||
| 660 | document.querySelector("#dataPointInfo").style.bottom = (point_height * item.record - 3) + "px"; | ||
| 661 | dataPoint.addEventListener("mouseenter", (e) => { | ||
| 662 | isDataActive = true; | ||
| 663 | isHoveringOverData = true; | ||
| 664 | const dataPoints = document.querySelectorAll(".data-point") | ||
| 665 | dataPoints.forEach(point => { | ||
| 666 | point.classList.remove("data-point-active") | ||
| 667 | }); | ||
| 668 | dataPoint.classList.add("data-point-active") | ||
| 669 | document.querySelector("#dataPointRecord").innerText = item.record; | ||
| 670 | document.querySelector("#dataPointMap").innerText = item.map; | ||
| 671 | document.querySelector("#dataPointDate").innerText = item.date.toLocaleDateString("en-GB"); | ||
| 672 | document.querySelector("#dataPointFirst").innerText = item.first; | ||
| 673 | if ((lineChart.clientWidth - 400) < item.x) { | ||
| 674 | document.querySelector("#dataPointInfo").style.left = item.x - 400 + "px"; | ||
| 675 | } else { | ||
| 676 | document.querySelector("#dataPointInfo").style.left = item.x + "px"; | ||
| 677 | } | ||
| 678 | if ((lineChart.clientHeight - 115) < (point_height * (item.record - minPortals) - 3)) { | ||
| 679 | document.querySelector("#dataPointInfo").style.bottom = (point_height * (item.record - minPortals) - 3) - 115 + "px"; | ||
| 680 | } else { | ||
| 681 | document.querySelector("#dataPointInfo").style.bottom = (point_height * (item.record - minPortals) - 3) + "px"; | ||
| 682 | } | ||
| 683 | document.querySelector("#dataPointInfo").style.opacity = "1"; | ||
| 684 | document.querySelector("#dataPointInfo").style.zIndex = "10"; | ||
| 685 | }); | ||
| 686 | document.querySelector("#dataPointInfo").addEventListener("mouseenter", (e) => { | ||
| 687 | isHoveringOverData = true; | ||
| 688 | }) | ||
| 689 | document.querySelector("#dataPointInfo").addEventListener("mouseleave", (e) => { | ||
| 690 | isHoveringOverData = false; | ||
| 691 | }) | ||
| 692 | document.addEventListener("mousedown", () => { | ||
| 693 | if (!isHoveringOverData) { | ||
| 694 | isDataActive = false | ||
| 695 | dataPoint.classList.remove("data-point-active") | ||
| 696 | document.querySelector("#dataPointInfo").style.opacity = "0"; | ||
| 697 | document.querySelector("#dataPointInfo").style.zIndex = "0"; | ||
| 698 | } | ||
| 699 | }) | ||
| 700 | dataPoint.addEventListener("mouseenter", (e) => { | ||
| 701 | isHoveringOverData = false; | ||
| 702 | }) | ||
| 703 | document.querySelector(".chart").addEventListener("mouseleave", () => { | ||
| 704 | isDataActive = false | ||
| 705 | // fuck you | ||
| 706 | isHoveringOverData = true; | ||
| 707 | dataPoint.classList.remove("data-point-active") | ||
| 708 | document.querySelector("#dataPointInfo").style.opacity = "0"; | ||
| 709 | document.querySelector("#dataPointInfo").style.zIndex = "0"; | ||
| 710 | }) | ||
| 711 | |||
| 712 | li.appendChild(lineSeg); | ||
| 713 | li.appendChild(dataPoint); | ||
| 714 | lineChart.appendChild(li); | ||
| 715 | } | ||
| 716 | } | ||
| 717 | |||
| 718 | async function fetchGames() { | ||
| 719 | try { | ||
| 720 | const response = await fetch("https://lp.ardapektezol.com/api/v1/games", { | ||
| 721 | headers: { | ||
| 722 | 'Authorization': token | ||
| 723 | } | ||
| 724 | }); | ||
| 725 | |||
| 726 | const data = await response.json(); | ||
| 727 | |||
| 728 | const gameImg = document.querySelector(".game-img"); | ||
| 729 | |||
| 730 | gameImg.style.backgroundImage = `url(${data.data[0].image})`; | ||
| 731 | |||
| 732 | // const mapImg = document.querySelectorAll(".maplist-img"); | ||
| 733 | // mapImg.forEach((map) => { | ||
| 734 | // map.style.backgroundImage = `url(${data.data[0].image})`; | ||
| 735 | // }); | ||
| 736 | |||
| 737 | } catch (error) { | ||
| 738 | console.log("error fetching games:", error); | ||
| 739 | } | ||
| 740 | } | ||
| 741 | |||
| 742 | detectGame(); | ||
| 743 | |||
| 744 | const maplistImg = document.querySelector("#maplistImg"); | ||
| 745 | maplistImg.src = img5; | ||
| 746 | const statisticsImg = document.querySelector("#statisticsImg"); | ||
| 747 | statisticsImg.src = img6; | ||
| 748 | |||
| 749 | fetchGames(); | ||
| 750 | |||
| 751 | const handleResize = (entries) => { | ||
| 752 | for (let entry of entries) { | ||
| 753 | if (hasOpenedStatistics) { | ||
| 754 | lineChart.innerHTML = "" | ||
| 755 | createGraph() | ||
| 756 | } | ||
| 757 | if (document.querySelector(".maplist").getAttribute("currentTab") == "stats") { | ||
| 758 | document.querySelector(".stats").style.display = "block" | ||
| 759 | } else { | ||
| 760 | document.querySelector(".stats").style.display = "none" | ||
| 761 | } | ||
| 762 | } | ||
| 763 | }; | ||
| 764 | |||
| 765 | const resizeObserver = new ResizeObserver(handleResize); | ||
| 766 | |||
| 767 | // if (scrollRef.current) { | ||
| 768 | // //hi | ||
| 769 | // if (new URLSearchParams(new URL(window.location.href).search).get("chapter")) { | ||
| 770 | // setTimeout(() => { | ||
| 771 | // scrollRef.current.scrollIntoView({ behavior: "smooth", block: "start" }) | ||
| 772 | // }, 200); | ||
| 773 | // } | ||
| 774 | |||
| 775 | // } | ||
| 776 | |||
| 777 | if (divRef.current) { | ||
| 778 | resizeObserver.observe(divRef.current); | ||
| 779 | } | ||
| 780 | |||
| 781 | return () => { | ||
| 782 | if (divRef.current) { | ||
| 783 | resizeObserver.unobserve(divRef.current); | ||
| 784 | } | ||
| 785 | resizeObserver.disconnect(); | ||
| 786 | }; | ||
| 787 | |||
| 788 | |||
| 789 | }) | ||
| 790 | return ( | ||
| 791 | <div ref={divRef} className='maplist-page'> | ||
| 792 | <div className='maplist-page-content'> | ||
| 793 | <section className='maplist-page-header'> | ||
| 794 | <Link to='/games'><button className='nav-btn'> | ||
| 795 | <i className='triangle'></i> | ||
| 796 | <span>Games list</span> | ||
| 797 | </button></Link> | ||
| 798 | {!loading ? | ||
| 799 | <span><b id='gameTitle'>{gameTitle}</b></span> | ||
| 800 | : | ||
| 801 | <span><b id='gameTitle' className='loader-text'>LOADINGLOADING</b></span>} | ||
| 802 | </section> | ||
| 803 | |||
| 804 | <div className='game'> | ||
| 805 | {!loading ? | ||
| 806 | <div className='game-header'> | ||
| 807 | <div className='game-img'></div> | ||
| 808 | <div className='game-header-text'> | ||
| 809 | <span><b id='catPortalCount'>{catPortalCount}</b></span> | ||
| 810 | <span>portals</span> | ||
| 811 | </div> | ||
| 812 | </div> | ||
| 813 | : <div className='game-header loader'> | ||
| 814 | <div className='game-img'></div> | ||
| 815 | <div className='game-header-text'> | ||
| 816 | <span className='loader-text'><b id='catPortalCount'>00</b></span> | ||
| 817 | <span className='loader-text'>portals</span> | ||
| 818 | </div> | ||
| 819 | </div>} | ||
| 820 | {!loading ? | ||
| 821 | <div className='game-nav'> | ||
| 822 | </div> | ||
| 823 | : <div className='game-nav loader'> | ||
| 824 | </div>} | ||
| 825 | </div> | ||
| 826 | |||
| 827 | <div className='gameview-nav'> | ||
| 828 | <button id='maplistBtn' onClick={() => { changeMaplistOrStatistics(0, "maplist") }} className='game-nav-btn selected'> | ||
| 829 | <img id='maplistImg' /> | ||
| 830 | <span>Map List</span> | ||
| 831 | </button> | ||
| 832 | <button id='maplistBtn' onClick={() => changeMaplistOrStatistics(1, "stats")} className='game-nav-btn'> | ||
| 833 | <img id='statisticsImg' /> | ||
| 834 | <span>Statistics</span> | ||
| 835 | </button> | ||
| 836 | </div> | ||
| 837 | |||
| 838 | <div ref={scrollRef} className='maplist'> | ||
| 839 | <div className='chapter'> | ||
| 840 | <span className='chapter-num'>undefined</span><br /> | ||
| 841 | <span className='chapter-name'>undefined</span> | ||
| 842 | |||
| 843 | <div className='chapter-page-div'> | ||
| 844 | <button id='pageChanger' onClick={() => { currentPage--; currentPage < minPage ? currentPage = minPage : changePage(currentPage); }}> | ||
| 845 | <i className='triangle'></i> | ||
| 846 | </button> | ||
| 847 | <span id='pageNumbers'>0/0</span> | ||
| 848 | <button id='pageChanger' onClick={() => { currentPage++; currentPage > maxPage ? currentPage = maxPage : changePage(currentPage); }}> | ||
| 849 | <i style={{ transform: "rotate(180deg)" }} className='triangle'></i> | ||
| 850 | </button> | ||
| 851 | </div> | ||
| 852 | |||
| 853 | <div className='maplist-maps'> | ||
| 854 | </div> | ||
| 855 | </div> | ||
| 856 | </div> | ||
| 857 | |||
| 858 | <div style={{ display: "block" }} className='stats'> | ||
| 859 | <div className='portalcount-over-time-div'> | ||
| 860 | <span className='graph-title'>Portal count over time</span><br /> | ||
| 861 | |||
| 862 | <div className='portalcount-graph'> | ||
| 863 | <figure className='chart'> | ||
| 864 | <div style={{ display: "block" }}></div> | ||
| 865 | <div id="dataPointInfo"> | ||
| 866 | <div className='section-header'> | ||
| 867 | <span className='header-title'>Date</span> | ||
| 868 | <span className='header-title'>Map</span> | ||
| 869 | <span className='header-title'>Record</span> | ||
| 870 | <span className='header-title'>First completion</span> | ||
| 871 | </div> | ||
| 872 | <div className='divider'></div> | ||
| 873 | <div className='section-data'> | ||
| 874 | <span id='dataPointDate'></span> | ||
| 875 | <span id='dataPointMap'></span> | ||
| 876 | <span id='dataPointRecord'></span> | ||
| 877 | <span id='dataPointFirst'>Hello</span> | ||
| 878 | </div> | ||
| 879 | </div> | ||
| 880 | <ul className='line-chart'> | ||
| 881 | |||
| 882 | </ul> | ||
| 883 | </figure> | ||
| 884 | </div> | ||
| 885 | </div> | ||
| 886 | </div> | ||
| 887 | </div> | ||
| 888 | </div> | ||
| 889 | ) | ||
| 890 | } \ No newline at end of file | ||
diff --git a/frontend/src/components/pages/profile.js b/frontend/src/components/pages/profile.js deleted file mode 100644 index 7c45320..0000000 --- a/frontend/src/components/pages/profile.js +++ /dev/null | |||
| @@ -1,382 +0,0 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { useLocation } from "react-router-dom"; | ||
| 3 | |||
| 4 | import img4 from "../../imgs/4.png" | ||
| 5 | import img5 from "../../imgs/5.png" | ||
| 6 | import img12 from "../../imgs/12.png" | ||
| 7 | import img13 from "../../imgs/13.png" | ||
| 8 | import img14 from "../../imgs/14.png" | ||
| 9 | import img15 from "../../imgs/15.png" | ||
| 10 | import img16 from "../../imgs/16.png" | ||
| 11 | import img17 from "../../imgs/17.png" | ||
| 12 | import img18 from "../../imgs/18.png" | ||
| 13 | import img19 from "../../imgs/19.png" | ||
| 14 | import "./profile.css"; | ||
| 15 | |||
| 16 | export default function Profile(props) { | ||
| 17 | const {token} = props | ||
| 18 | |||
| 19 | |||
| 20 | const location = useLocation() | ||
| 21 | |||
| 22 | |||
| 23 | const [profileData, setProfileData] = React.useState(null) | ||
| 24 | React.useEffect(()=>{ | ||
| 25 | setProfileData(null) | ||
| 26 | setChapterData(null) | ||
| 27 | setMaps(null) | ||
| 28 | setPageNumber(1) | ||
| 29 | |||
| 30 | if(location.pathname==="/profile"){ | ||
| 31 | fetch(`https://lp.ardapektezol.com/api/v1/${location.pathname}`,{ | ||
| 32 | headers: { | ||
| 33 | 'Authorization': token | ||
| 34 | }}) | ||
| 35 | .then(r=>r.json()) | ||
| 36 | .then(d=>{ | ||
| 37 | setProfileData(d.data) | ||
| 38 | setPageMax(Math.ceil(d.data.records.length/20)) | ||
| 39 | }) | ||
| 40 | }else{ | ||
| 41 | fetch(`https://lp.ardapektezol.com/api/v1/${location.pathname}`) | ||
| 42 | .then(r=>r.json()) | ||
| 43 | .then(d=>{ | ||
| 44 | setProfileData(d.data) | ||
| 45 | setPageMax(Math.ceil(d.data.records.length/20)) | ||
| 46 | }) | ||
| 47 | } | ||
| 48 | },[location.pathname]) | ||
| 49 | |||
| 50 | |||
| 51 | |||
| 52 | const [game,setGame] = React.useState(0) | ||
| 53 | const [gameData,setGameData] = React.useState(null) | ||
| 54 | const [chapter,setChapter] = React.useState("0") | ||
| 55 | const [chapterData,setChapterData] = React.useState(null) | ||
| 56 | const [maps,setMaps] = React.useState(null) | ||
| 57 | |||
| 58 | React.useEffect(()=>{ | ||
| 59 | fetch("https://lp.ardapektezol.com/api/v1/games") | ||
| 60 | .then(r=>r.json()) | ||
| 61 | .then(d=>{ | ||
| 62 | setGameData(d.data) | ||
| 63 | setGame(0) | ||
| 64 | }) | ||
| 65 | |||
| 66 | },[location]) | ||
| 67 | |||
| 68 | React.useEffect(()=>{ | ||
| 69 | if(game!==null && game!= 0){ | ||
| 70 | fetch(`https://lp.ardapektezol.com/api/v1/games/${game}`) | ||
| 71 | .then(r=>r.json()) | ||
| 72 | .then(d=>{ | ||
| 73 | setChapterData(d.data) | ||
| 74 | setChapter("0") | ||
| 75 | document.querySelector('#select-chapter').value=0 | ||
| 76 | }) | ||
| 77 | |||
| 78 | } else if (game!==null && game==0 && profileData!== null){ | ||
| 79 | setPageMax(Math.ceil(profileData.records.length/20)) | ||
| 80 | setPageNumber(1) | ||
| 81 | } | ||
| 82 | |||
| 83 | },[game,location]) | ||
| 84 | |||
| 85 | React.useEffect(()=>{ | ||
| 86 | if(chapter!==null){ | ||
| 87 | if(chapter==0){ | ||
| 88 | setMaps(null) | ||
| 89 | fetch(`https://lp.ardapektezol.com/api/v1/games/${game}/maps`) | ||
| 90 | .then(r=>r.json()) | ||
| 91 | .then(d=>{ | ||
| 92 | setMaps(d.data.maps); | ||
| 93 | setPageMax(Math.ceil(d.data.maps.length/20)) | ||
| 94 | setPageNumber(1) | ||
| 95 | }) | ||
| 96 | }else{ | ||
| 97 | setMaps(null) | ||
| 98 | fetch(`https://lp.ardapektezol.com/api/v1/chapters/${chapter}`) | ||
| 99 | .then(r=>r.json()) | ||
| 100 | .then(d=>{ | ||
| 101 | setMaps(d.data.maps); | ||
| 102 | setPageMax(Math.ceil(d.data.maps.length/20)) | ||
| 103 | setPageNumber(1) | ||
| 104 | }) | ||
| 105 | |||
| 106 | } | ||
| 107 | } | ||
| 108 | },[chapter,chapterData]) | ||
| 109 | |||
| 110 | |||
| 111 | |||
| 112 | const [pageNumber, setPageNumber] = React.useState(1); | ||
| 113 | const [pageMax, setPageMax] = React.useState(0); | ||
| 114 | const [navState, setNavState] = React.useState(0); // eslint-disable-next-line | ||
| 115 | React.useEffect(() => {NavClick();}, [[],navState]); | ||
| 116 | function NavClick() { | ||
| 117 | if(profileData!==null){ | ||
| 118 | const btn = document.querySelectorAll("#section2 button"); | ||
| 119 | btn.forEach((e) => {e.style.backgroundColor = "#2b2e46"}); | ||
| 120 | btn[navState].style.backgroundColor = "#202232"; | ||
| 121 | |||
| 122 | document.querySelectorAll("section").forEach((e,i)=>i>=2?e.style.display="none":"") | ||
| 123 | if(navState === 0){document.querySelectorAll(".profile1").forEach((e) => {e.style.display = "block"});} | ||
| 124 | if(navState === 1){document.querySelectorAll(".profile2").forEach((e) => {e.style.display = "block"});} | ||
| 125 | } | ||
| 126 | } | ||
| 127 | function UpdateProfile(){ | ||
| 128 | fetch(`https://lp.ardapektezol.com/api/v1/profile`,{ | ||
| 129 | method: 'POST', | ||
| 130 | headers: {Authorization: token} | ||
| 131 | }).then(r=>r.json()) | ||
| 132 | .then(d=>d.success?window.alert("profile updated"):window.alert(`Error: ${d.message}`)) | ||
| 133 | } | ||
| 134 | |||
| 135 | function TimeAgo(date) { | ||
| 136 | const seconds = Math.floor((new Date() - date) / 1000); | ||
| 137 | |||
| 138 | let interval = Math.floor(seconds / 31536000); | ||
| 139 | if (interval > 1) {return interval + ' years ago';} | ||
| 140 | |||
| 141 | interval = Math.floor(seconds / 2592000); | ||
| 142 | if (interval > 1) {return interval + ' months ago';} | ||
| 143 | |||
| 144 | interval = Math.floor(seconds / 86400); | ||
| 145 | if (interval > 1) {return interval + ' days ago';} | ||
| 146 | |||
| 147 | interval = Math.floor(seconds / 3600); | ||
| 148 | if (interval > 1) {return interval + ' hours ago';} | ||
| 149 | |||
| 150 | interval = Math.floor(seconds / 60); | ||
| 151 | if (interval > 1) {return interval + ' minutes ago';} | ||
| 152 | |||
| 153 | if(seconds < 10) return 'just now'; | ||
| 154 | |||
| 155 | return Math.floor(seconds) + ' seconds ago'; | ||
| 156 | }; | ||
| 157 | |||
| 158 | function TicksToTime(ticks) { | ||
| 159 | |||
| 160 | let seconds = Math.floor(ticks/60) | ||
| 161 | let minutes = Math.floor(seconds/60) | ||
| 162 | let hours = Math.floor(minutes/60) | ||
| 163 | |||
| 164 | let milliseconds = Math.floor((ticks%60)*1000/60) | ||
| 165 | seconds = seconds % 60; | ||
| 166 | minutes = minutes % 60; | ||
| 167 | |||
| 168 | 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')} (${ticks})`; | ||
| 169 | } | ||
| 170 | |||
| 171 | |||
| 172 | if(profileData!==null){ | ||
| 173 | return ( | ||
| 174 | <main> | ||
| 175 | <section id='section1' className='profile'> | ||
| 176 | |||
| 177 | {profileData.profile?( | ||
| 178 | <div id='profile-image' onClick={()=>UpdateProfile()}> | ||
| 179 | <img src={profileData.avatar_link} alt=""></img> | ||
| 180 | <span>Refresh</span> | ||
| 181 | </div> | ||
| 182 | ):( | ||
| 183 | <div> | ||
| 184 | <img src={profileData.avatar_link} alt=""></img> | ||
| 185 | </div> | ||
| 186 | )} | ||
| 187 | |||
| 188 | <div id='profile-top'> | ||
| 189 | <div> | ||
| 190 | <div>{profileData.user_name}</div> | ||
| 191 | <div> | ||
| 192 | {profileData.country_code==="XX"?"":<img src={`https://flagcdn.com/w80/${profileData.country_code.toLowerCase()}.jpg`} alt={profileData.country_code} />} | ||
| 193 | </div> | ||
| 194 | <div> | ||
| 195 | {profileData.titles.map(e=>( | ||
| 196 | <span className="titles" style={{backgroundColor:`#${e.color}`}}> | ||
| 197 | {e.name} | ||
| 198 | </span> | ||
| 199 | ))} | ||
| 200 | </div> | ||
| 201 | </div> | ||
| 202 | <div> | ||
| 203 | {profileData.links.p2sr==="-"?"":<a href={profileData.links.p2sr}><img src={img17} alt="Steam" /></a>} | ||
| 204 | {profileData.links.p2sr==="-"?"":<a href={profileData.links.p2sr}><img src={img15} alt="Twitch" /></a>} | ||
| 205 | {profileData.links.p2sr==="-"?"":<a href={profileData.links.p2sr}><img src={img16} alt="Youtube" /></a>} | ||
| 206 | {profileData.links.p2sr==="-"?"":<a href={profileData.links.p2sr}><img src={img4} alt="P2SR" style={{padding:"0"}} /></a>} | ||
| 207 | </div> | ||
| 208 | |||
| 209 | </div> | ||
| 210 | <div id='profile-bottom'> | ||
| 211 | <div> | ||
| 212 | <span>Overall</span> | ||
| 213 | <span>{profileData.rankings.overall.rank===0?"N/A ":"#"+profileData.rankings.overall.rank+" "} | ||
| 214 | <span>({profileData.rankings.overall.completion_count}/{profileData.rankings.overall.completion_total})</span> | ||
| 215 | </span> | ||
| 216 | </div> | ||
| 217 | <div> | ||
| 218 | <span>Singleplayer</span> | ||
| 219 | <span>{profileData.rankings.singleplayer.rank===0?"N/A ":"#"+profileData.rankings.singleplayer.rank+" "} | ||
| 220 | <span>({profileData.rankings.singleplayer.completion_count}/{profileData.rankings.singleplayer.completion_total})</span> | ||
| 221 | </span> | ||
| 222 | </div> | ||
| 223 | <div> | ||
| 224 | <span>Cooperative</span> | ||
| 225 | <span>{profileData.rankings.cooperative.rank===0?"N/A ":"#"+profileData.rankings.cooperative.rank+" "} | ||
| 226 | <span>({profileData.rankings.cooperative.completion_count}/{profileData.rankings.cooperative.completion_total})</span> | ||
| 227 | </span> | ||
| 228 | </div> | ||
| 229 | </div> | ||
| 230 | </section> | ||
| 231 | |||
| 232 | |||
| 233 | <section id='section2' className='profile'> | ||
| 234 | <button onClick={()=>setNavState(0)}><img src={img5} alt="" /> Player Records</button> | ||
| 235 | <button onClick={()=>setNavState(1)}><img src={img14} alt="" /> Statistics</button> | ||
| 236 | </section> | ||
| 237 | |||
| 238 | |||
| 239 | |||
| 240 | |||
| 241 | |||
| 242 | <section id='section3' className='profile1'> | ||
| 243 | <div id='profileboard-nav'> | ||
| 244 | {gameData===null?<select>error</select>: | ||
| 245 | |||
| 246 | <select id='select-game' | ||
| 247 | onChange={()=>setGame(document.querySelector('#select-game').value)}> | ||
| 248 | <option value={0} key={0}>All Scores</option> | ||
| 249 | {gameData.map((e,i)=>( | ||
| 250 | <option value={e.id} key={i+1}>{e.name}</option> | ||
| 251 | ))}</select> | ||
| 252 | } | ||
| 253 | |||
| 254 | {game==0? | ||
| 255 | <select disabled> | ||
| 256 | <option>All Scores</option> | ||
| 257 | </select> | ||
| 258 | :chapterData===null?<select></select>: | ||
| 259 | |||
| 260 | <select id='select-chapter' | ||
| 261 | onChange={()=>setChapter(document.querySelector('#select-chapter').value)}> | ||
| 262 | <option value="0" key="0">All</option> | ||
| 263 | {chapterData.chapters.filter(e=>e.is_disabled===false).map((e,i)=>( | ||
| 264 | <option value={e.id} key={i+1}>{e.name}</option> | ||
| 265 | ))}</select> | ||
| 266 | } | ||
| 267 | </div> | ||
| 268 | <div id='profileboard-top'> | ||
| 269 | <span><span>Map Name</span><img src={img19} alt="" /></span> | ||
| 270 | <span style={{justifyContent:'center'}}><span>Portals</span><img src={img19} alt="" /></span> | ||
| 271 | <span style={{justifyContent:'center'}}><span>WRΔ </span><img src={img19} alt="" /></span> | ||
| 272 | <span style={{justifyContent:'center'}}><span>Time</span><img src={img19} alt="" /></span> | ||
| 273 | <span> </span> | ||
| 274 | <span><span>Rank</span><img src={img19} alt="" /></span> | ||
| 275 | <span><span>Date</span><img src={img19} alt="" /></span> | ||
| 276 | <div id='page-number'> | ||
| 277 | <div> | ||
| 278 | <button onClick={() => pageNumber === 1 ? null : setPageNumber(prevPageNumber => prevPageNumber - 1)} | ||
| 279 | ><i className='triangle' style={{position:'relative',left:'-5px',}}></i> </button> | ||
| 280 | <span>{pageNumber}/{pageMax}</span> | ||
| 281 | <button onClick={() => pageNumber === pageMax? null : setPageNumber(prevPageNumber => prevPageNumber + 1)} | ||
| 282 | ><i className='triangle' style={{position:'relative',left:'5px',transform:'rotate(180deg)'}}></i> </button> | ||
| 283 | </div> | ||
| 284 | </div> | ||
| 285 | </div> | ||
| 286 | <hr/> | ||
| 287 | <div id='profileboard-records'> | ||
| 288 | |||
| 289 | {game == 0 && profileData !== null | ||
| 290 | ? ( | ||
| 291 | |||
| 292 | profileData.records.sort((a,b)=>a.map_id - b.map_id) | ||
| 293 | .map((r, index) => ( | ||
| 294 | |||
| 295 | Math.ceil((index+1)/20)===pageNumber ? ( | ||
| 296 | <button className="profileboard-record" key={index}> | ||
| 297 | {r.scores.map((e,i)=>(<> | ||
| 298 | {i!==0?<hr style={{gridColumn:"1 / span 8"}}/>:""} | ||
| 299 | |||
| 300 | <span>{r.map_name}</span> | ||
| 301 | |||
| 302 | <span style={{ display: "grid" }}>{e.score_count}</span> | ||
| 303 | |||
| 304 | <span style={{ display: "grid" }}>{e.score_count-r.map_wr_count}</span> | ||
| 305 | <span style={{ display: "grid" }}>{TicksToTime(e.score_time)}</span> | ||
| 306 | <span> </span> | ||
| 307 | {i===0?<span>#{r.placement}</span>:<span> </span>} | ||
| 308 | <span>{e.date.split("T")[0]}</span> | ||
| 309 | <span style={{ flexDirection: "row-reverse" }}> | ||
| 310 | |||
| 311 | <button onClick={()=>{window.alert(`Demo ID: ${e.demo_id}`)}}><img src={img13} alt="demo_id" /></button> | ||
| 312 | <button onClick={()=>window.location.href=`https://lp.ardapektezol.com/api/v1/demos?uuid=${e.demo_id}`}><img src={img12} alt="download" /></button> | ||
| 313 | {i===0&&r.scores.length>1?<button onClick={()=> | ||
| 314 | { | ||
| 315 | document.querySelectorAll(".profileboard-record")[index%20].style.height==="44px"|| | ||
| 316 | document.querySelectorAll(".profileboard-record")[index%20].style.height===""? | ||
| 317 | document.querySelectorAll(".profileboard-record")[index%20].style.height=`${r.scores.length*46}px`: | ||
| 318 | document.querySelectorAll(".profileboard-record")[index%20].style.height="44px" | ||
| 319 | } | ||
| 320 | }><img src={img18} alt="history" /></button>:""} | ||
| 321 | |||
| 322 | </span> | ||
| 323 | </>))} | ||
| 324 | |||
| 325 | </button> | ||
| 326 | ) : "" | ||
| 327 | ))) : maps !== null ? | ||
| 328 | |||
| 329 | maps.filter(e=>e.is_disabled===false).sort((a,b)=>a.id - b.id) | ||
| 330 | .map((r, index) => { | ||
| 331 | if(Math.ceil((index+1)/20)===pageNumber){ | ||
| 332 | let record = profileData.records.find((e) => e.map_id === r.id); | ||
| 333 | return record === undefined ? ( | ||
| 334 | <button className="profileboard-record" key={index} style={{backgroundColor:"#1b1b20"}}> | ||
| 335 | <span>{r.name}</span> | ||
| 336 | <span style={{ display: "grid" }}>N/A</span> | ||
| 337 | <span style={{ display: "grid" }}>N/A</span> | ||
| 338 | <span>N/A</span> | ||
| 339 | <span> </span> | ||
| 340 | <span>N/A</span> | ||
| 341 | <span>N/A</span> | ||
| 342 | <span style={{ flexDirection: "row-reverse" }}></span> | ||
| 343 | </button> | ||
| 344 | ) : ( | ||
| 345 | <button className="profileboard-record" key={index}> | ||
| 346 | {record.scores.map((e,i)=>(<> | ||
| 347 | {i!==0?<hr style={{gridColumn:"1 / span 8"}}/>:""} | ||
| 348 | <span>{r.name}</span> | ||
| 349 | <span style={{ display: "grid" }}>{record.scores[i].score_count}</span> | ||
| 350 | <span style={{ display: "grid" }}>{record.scores[i].score_count-record.map_wr_count}</span> | ||
| 351 | <span style={{ display: "grid" }}>{TicksToTime(record.scores[i].score_time)}</span> | ||
| 352 | <span> </span> | ||
| 353 | {i===0?<span>#{record.placement}</span>:<span> </span>} | ||
| 354 | <span>{record.scores[i].date.split("T")[0]}</span> | ||
| 355 | <span style={{ flexDirection: "row-reverse" }}> | ||
| 356 | |||
| 357 | <button onClick={()=>{window.alert(`Demo ID: ${e.demo_id}`)}}><img src={img13} alt="demo_id" /></button> | ||
| 358 | <button onClick={()=>window.location.href=`https://lp.ardapektezol.com/api/v1/demos?uuid=${e.demo_id}`}><img src={img12} alt="download" /></button> | ||
| 359 | {i===0&&record.scores.length>1?<button onClick={()=> | ||
| 360 | { | ||
| 361 | document.querySelectorAll(".profileboard-record")[index%20].style.height==="44px"|| | ||
| 362 | document.querySelectorAll(".profileboard-record")[index%20].style.height===""? | ||
| 363 | document.querySelectorAll(".profileboard-record")[index%20].style.height=`${record.scores.length*46}px`: | ||
| 364 | document.querySelectorAll(".profileboard-record")[index%20].style.height="44px" | ||
| 365 | } | ||
| 366 | }><img src={img18} alt="history" /></button>:""} | ||
| 367 | |||
| 368 | </span> | ||
| 369 | </>))} | ||
| 370 | </button> | ||
| 371 | |||
| 372 | ) | ||
| 373 | }else{return null} | ||
| 374 | }):(<>{console.warn(maps)}</>)} | ||
| 375 | </div> | ||
| 376 | </section> | ||
| 377 | |||
| 378 | </main> | ||
| 379 | )} | ||
| 380 | } | ||
| 381 | |||
| 382 | |||
diff --git a/frontend/src/components/pages/summary.js b/frontend/src/components/pages/summary.js deleted file mode 100644 index d276408..0000000 --- a/frontend/src/components/pages/summary.js +++ /dev/null | |||
| @@ -1,650 +0,0 @@ | |||
| 1 | import React, { useEffect } from 'react'; | ||
| 2 | import { useLocation, Link } from "react-router-dom"; | ||
| 3 | import ReactMarkdown from 'react-markdown' | ||
| 4 | |||
| 5 | import "./summary.css"; | ||
| 6 | |||
| 7 | import img4 from "../../imgs/4.png" | ||
| 8 | import img5 from "../../imgs/5.png" | ||
| 9 | import img6 from "../../imgs/6.png" | ||
| 10 | import img12 from "../../imgs/12.png" | ||
| 11 | import img13 from "../../imgs/13.png" | ||
| 12 | import Modview from "./summary_modview.js" | ||
| 13 | |||
| 14 | export default function Summary(prop) { | ||
| 15 | const {token,mod} = prop | ||
| 16 | const fakedata={} //for debug | ||
| 17 | |||
| 18 | const location = useLocation() | ||
| 19 | |||
| 20 | //fetching data | ||
| 21 | const [data, setData] = React.useState(null); | ||
| 22 | React.useEffect(() => { | ||
| 23 | setData(null) | ||
| 24 | setDiscussionThread(null) | ||
| 25 | setCreatePostState(0) | ||
| 26 | setSelectedRun(0) | ||
| 27 | setCatState(1) | ||
| 28 | fetch(`https://lp.ardapektezol.com/api/v1/maps/${location.pathname.split('/')[2]}/summary`) | ||
| 29 | .then(r => r.json()) | ||
| 30 | .then(d => { | ||
| 31 | if(Object.keys(fakedata).length!==0){setData(fakedata)} | ||
| 32 | else{setData(d.data)} | ||
| 33 | if(d.data.summary.routes.length===0){d.data.summary.routes[0]={"category": "","history": {"score_count": 0,},"rating": 0,"description": "","showcase": ""}} | ||
| 34 | }) | ||
| 35 | // eslint-disable-next-line | ||
| 36 | }, [location.pathname]); | ||
| 37 | |||
| 38 | const [pageNumber, setPageNumber] = React.useState(1); | ||
| 39 | const [lbData, setLbData] = React.useState(null); | ||
| 40 | React.useEffect(() => { | ||
| 41 | fetch(`https://lp.ardapektezol.com/api/v1/maps/${location.pathname.split('/')[2]}/leaderboards?page=${pageNumber}`) | ||
| 42 | .then(r => r.json()) | ||
| 43 | .then(d => setLbData(d)) | ||
| 44 | // eslint-disable-next-line | ||
| 45 | }, [pageNumber,location.pathname]); | ||
| 46 | |||
| 47 | const [discussions,setDiscussions] = React.useState(null) | ||
| 48 | function fetchDiscussions() { | ||
| 49 | fetch(`https://lp.ardapektezol.com/api/v1/maps/${location.pathname.split('/')[2]}/discussions`) | ||
| 50 | .then(r=>r.json()) | ||
| 51 | .then(d=>setDiscussions(d.data.discussions)) | ||
| 52 | } | ||
| 53 | |||
| 54 | React.useEffect(()=>{ | ||
| 55 | fetchDiscussions() | ||
| 56 | },[location.pathname]) | ||
| 57 | |||
| 58 | |||
| 59 | |||
| 60 | const [discussionThread,setDiscussionThread] = React.useState(null) | ||
| 61 | function openDiscussion(x){ | ||
| 62 | fetch(`https://lp.ardapektezol.com/api/v1/maps/${location.pathname.split('/')[2]}/discussions/${x}`) | ||
| 63 | .then(r=>r.json()) | ||
| 64 | .then(d=>setDiscussionThread(d.data.discussion)) | ||
| 65 | } | ||
| 66 | const [discussionSearch, setDiscussionSearch] = React.useState("") | ||
| 67 | |||
| 68 | |||
| 69 | |||
| 70 | |||
| 71 | const [navState, setNavState] = React.useState(0); // eslint-disable-next-line | ||
| 72 | React.useEffect(() => {NavClick();}, [[],navState]); | ||
| 73 | |||
| 74 | function NavClick() { | ||
| 75 | if(data!==null){ | ||
| 76 | const btn = document.querySelectorAll("#section2 button.nav-button"); | ||
| 77 | btn.forEach((e) => {e.style.backgroundColor = "#2b2e46"}); | ||
| 78 | btn[navState].style.backgroundColor = "#202232"; | ||
| 79 | |||
| 80 | document.querySelectorAll("section").forEach((e,i)=>i>=2?e.style.display="none":"") | ||
| 81 | if(navState === 0){document.querySelectorAll(".summary1").forEach((e) => {e.style.display = "grid"});} | ||
| 82 | if(navState === 1){document.querySelectorAll(".summary2").forEach((e) => {e.style.display = "block"});} | ||
| 83 | if(navState === 2){document.querySelectorAll(".summary3").forEach((e) => {e.style.display = "block"});} | ||
| 84 | }} | ||
| 85 | |||
| 86 | |||
| 87 | const [catState, setCatState] = React.useState(1); // eslint-disable-next-line | ||
| 88 | React.useEffect(() => {CatClick();}, [[],catState]); | ||
| 89 | |||
| 90 | function CatClick() { | ||
| 91 | if(data!==null){ | ||
| 92 | const btn = document.querySelectorAll("#section3 #category span button"); | ||
| 93 | btn.forEach((e) => {e.style.backgroundColor = "#2b2e46"}); | ||
| 94 | btn[catState-1].style.backgroundColor = "#202232"; | ||
| 95 | }} | ||
| 96 | React.useEffect(()=>{ | ||
| 97 | if(data!==null && data.summary.routes.filter(e=>e.category.id===catState).length!==0){ | ||
| 98 | selectRun(0,catState)} // eslint-disable-next-line | ||
| 99 | },[catState,data]) | ||
| 100 | |||
| 101 | |||
| 102 | const [hisState, setHisState] = React.useState(0); // eslint-disable-next-line | ||
| 103 | React.useEffect(() => {HisClick();}, [[],hisState]); | ||
| 104 | |||
| 105 | function HisClick() { | ||
| 106 | if(data!==null){ | ||
| 107 | const btn = document.querySelectorAll("#section3 #history span button"); | ||
| 108 | btn.forEach((e) => {e.style.backgroundColor = "#2b2e46"}); | ||
| 109 | btn[hisState].style.backgroundColor = "#202232"; | ||
| 110 | |||
| 111 | }} | ||
| 112 | |||
| 113 | const [selectedRun,setSelectedRun] = React.useState(0) | ||
| 114 | |||
| 115 | function selectRun(x,y){ | ||
| 116 | let r = document.querySelectorAll("button.record") | ||
| 117 | r.forEach(e=>e.style.backgroundColor="#2b2e46") | ||
| 118 | r[x].style.backgroundColor="#161723" | ||
| 119 | |||
| 120 | |||
| 121 | if(data!==null && data.summary.routes.length!==0 && data.summary.routes.length!==0){ | ||
| 122 | if(y===2){x+=data.summary.routes.filter(e=>e.category.id<2).length} | ||
| 123 | if(y===3){x+=data.summary.routes.filter(e=>e.category.id<3).length} | ||
| 124 | if(y===4){x+=data.summary.routes.filter(e=>e.category.id<4).length} | ||
| 125 | setSelectedRun(x) | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | function graph(state) { | ||
| 130 | // this is such a mess | ||
| 131 | let graph = data.summary.routes.filter(e=>e.category.id===catState) | ||
| 132 | let graph_score = [] | ||
| 133 | data.summary.routes.filter(e=>e.category.id===catState).forEach(e=>graph_score.push(e.history.score_count)) | ||
| 134 | let graph_dates = [] | ||
| 135 | data.summary.routes.filter(e=>e.category.id===catState).forEach(e=>graph_dates.push(e.history.date.split("T")[0])) | ||
| 136 | let graph_max = graph[graph.length-1].history.score_count | ||
| 137 | let graph_numbers = [] | ||
| 138 | for (let i=graph_max;i>=0;i--){ | ||
| 139 | graph_numbers[i]=i | ||
| 140 | } | ||
| 141 | |||
| 142 | switch (state) { | ||
| 143 | case 1: //numbers | ||
| 144 | return graph_numbers | ||
| 145 | .reverse().map(e=>( | ||
| 146 | graph_score.includes(e) || e===0 ? | ||
| 147 | <span>{e}<br/></span> | ||
| 148 | : | ||
| 149 | <span><br/></span> | ||
| 150 | )) | ||
| 151 | case 2: // graph | ||
| 152 | let g = 0 | ||
| 153 | let h = 0 | ||
| 154 | return graph_numbers.map((e,j)=>( | ||
| 155 | <tr id={'graph_row-'+(graph_max-j)} | ||
| 156 | data-graph={ graph_score.includes(graph_max-j) ? g++ : 0} | ||
| 157 | data-graph2={h=0} | ||
| 158 | |||
| 159 | > | ||
| 160 | { | ||
| 161 | graph_score.map((e,i)=>( | ||
| 162 | <> | ||
| 163 | <td className='graph_ver' | ||
| 164 | data-graph={ h++ } | ||
| 165 | style={{outline: | ||
| 166 | g===h-1 ? | ||
| 167 | "1px solid #2b2e46" : g>=h ? "1px dashed white" : "0" }} | ||
| 168 | ></td> | ||
| 169 | |||
| 170 | {g===h && graph_score.includes(graph_max-j) ? | ||
| 171 | <button className='graph-button' | ||
| 172 | onClick={()=>{ | ||
| 173 | selectRun(graph_dates.length-(i-1),catState); | ||
| 174 | }} | ||
| 175 | style={{left: `calc(100% / ${graph_dates.length} * ${h-1})`}} | ||
| 176 | ></button> | ||
| 177 | : ""} | ||
| 178 | |||
| 179 | <td className='graph_hor' id={'graph_table-'+i++} | ||
| 180 | style={{ | ||
| 181 | outline: | ||
| 182 | graph_score.includes(graph_max-j) ? | ||
| 183 | g>=h ? | ||
| 184 | g-1>=h ? "1px dashed #2b2e46" : "1px solid white" : "0" | ||
| 185 | : "0"}} | ||
| 186 | ></td> | ||
| 187 | |||
| 188 | |||
| 189 | |||
| 190 | <td className='graph_hor' id={'graph_table-'+i++} | ||
| 191 | style={{outline: | ||
| 192 | graph_score.includes(graph_max-j) ? | ||
| 193 | g>=h ? | ||
| 194 | g-1>=h ? "1px dashed #2b2e46" : "1px solid white" : "0" | ||
| 195 | : "0"}} | ||
| 196 | ></td> | ||
| 197 | |||
| 198 | </> | ||
| 199 | )) | ||
| 200 | |||
| 201 | } | ||
| 202 | |||
| 203 | </tr> | ||
| 204 | )) | ||
| 205 | |||
| 206 | case 3: // dates | ||
| 207 | return graph_dates | ||
| 208 | .reverse().map(e=>( | ||
| 209 | <span>{e}</span> | ||
| 210 | )) | ||
| 211 | default: | ||
| 212 | break; | ||
| 213 | |||
| 214 | } | ||
| 215 | |||
| 216 | } | ||
| 217 | |||
| 218 | const [vid,setVid] = React.useState("") | ||
| 219 | React.useEffect(()=>{ | ||
| 220 | if(data!==null){ | ||
| 221 | let showcase = data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].showcase | ||
| 222 | showcase.length>6 ? setVid("https://www.youtube.com/embed/"+YouTubeGetID(showcase)) | ||
| 223 | : setVid("") | ||
| 224 | } // eslint-disable-next-line | ||
| 225 | },[[],selectedRun]) | ||
| 226 | |||
| 227 | function YouTubeGetID(url){ | ||
| 228 | url = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/); | ||
| 229 | return (url[2] !== undefined) ? url[2].split(/[^0-9a-z_]/i)[0] : url[0]; | ||
| 230 | } | ||
| 231 | |||
| 232 | function TimeAgo(date) { | ||
| 233 | // const seconds = Math.floor((new Date() - date) / 1000); | ||
| 234 | |||
| 235 | const seconds = Math.floor(((new Date(new Date() - (date.getTimezoneOffset()*-60000))) - date) / 1000); | ||
| 236 | |||
| 237 | let interval = Math.floor(seconds / 31536000); | ||
| 238 | if (interval === 1) {return interval + ' year ago';} | ||
| 239 | if (interval > 1) {return interval + ' years ago';} | ||
| 240 | |||
| 241 | interval = Math.floor(seconds / 2592000); | ||
| 242 | if (interval === 1) {return interval + ' month ago';} | ||
| 243 | if (interval > 1) {return interval + ' months ago';} | ||
| 244 | |||
| 245 | interval = Math.floor(seconds / 86400); | ||
| 246 | if (interval === 1) {return interval + ' day ago';} | ||
| 247 | if (interval > 1) {return interval + ' days ago';} | ||
| 248 | |||
| 249 | interval = Math.floor(seconds / 3600); | ||
| 250 | if (interval === 1) {return interval + ' hour ago';} | ||
| 251 | if (interval > 1) {return interval + ' hours ago';} | ||
| 252 | |||
| 253 | interval = Math.floor(seconds / 60); | ||
| 254 | if (interval === 1) {return interval + ' minute ago';} | ||
| 255 | if (interval > 1) {return interval + ' minutes ago';} | ||
| 256 | |||
| 257 | if(seconds < 10) return 'just now'; | ||
| 258 | |||
| 259 | return Math.floor(seconds) + ' seconds ago'; | ||
| 260 | }; | ||
| 261 | |||
| 262 | function TicksToTime(ticks) { | ||
| 263 | |||
| 264 | let seconds = Math.floor(ticks/60) | ||
| 265 | let minutes = Math.floor(seconds/60) | ||
| 266 | let hours = Math.floor(minutes/60) | ||
| 267 | |||
| 268 | let milliseconds = Math.floor((ticks%60)*1000/60) | ||
| 269 | seconds = seconds % 60; | ||
| 270 | minutes = minutes % 60; | ||
| 271 | |||
| 272 | 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')} (${ticks})`; | ||
| 273 | } | ||
| 274 | |||
| 275 | function PostComment() { | ||
| 276 | |||
| 277 | fetch(`https://lp.ardapektezol.com/api/v1/maps/${location.pathname.split('/')[2]}/discussions/${discussionThread.id}`,{ | ||
| 278 | method:"POST", | ||
| 279 | headers:{authorization:token}, | ||
| 280 | body:JSON.stringify({"comment":document.querySelector("#discussion-send>input").value}) | ||
| 281 | }) | ||
| 282 | .then(r=>r.json()) | ||
| 283 | .then(d=>{ | ||
| 284 | document.querySelector("#discussion-send>input").value="" | ||
| 285 | openDiscussion(discussionThread.id) | ||
| 286 | }) | ||
| 287 | } | ||
| 288 | |||
| 289 | |||
| 290 | const [createPostState,setCreatePostState] = React.useState(0) | ||
| 291 | function CreatePost() { | ||
| 292 | |||
| 293 | fetch(`https://lp.ardapektezol.com/api/v1/maps/${location.pathname.split('/')[2]}/discussions`,{ | ||
| 294 | method:"POST", | ||
| 295 | headers:{authorization:token}, | ||
| 296 | body:JSON.stringify({"title":document.querySelector("#discussion-create-title").value,"content":document.querySelector("#discussion-create-content").value}) | ||
| 297 | }) | ||
| 298 | .then(r=>r.json()) | ||
| 299 | .then(d=>{ | ||
| 300 | setCreatePostState(0) | ||
| 301 | fetchDiscussions() | ||
| 302 | }) | ||
| 303 | } | ||
| 304 | |||
| 305 | function DeletePost(post) { | ||
| 306 | if(window.confirm(`Are you sure you want to remove post: ${post.title}?`)){ | ||
| 307 | console.log("deleted",post.id) | ||
| 308 | fetch(`https://lp.ardapektezol.com/api/v1/maps/${location.pathname.split('/')[2]}/discussions/${post.id}`,{ | ||
| 309 | method:"DELETE", | ||
| 310 | headers:{authorization:token}, | ||
| 311 | }) | ||
| 312 | .then(r=>r.json()) | ||
| 313 | .then(d=>{ | ||
| 314 | fetchDiscussions() | ||
| 315 | }) | ||
| 316 | } | ||
| 317 | } | ||
| 318 | |||
| 319 | |||
| 320 | if(data!==null){ | ||
| 321 | console.log(data) | ||
| 322 | |||
| 323 | let current_chapter = data.map.chapter_name | ||
| 324 | let isCoop = false; | ||
| 325 | if (data.map.game_name == "Portal 2 - Cooperative") { | ||
| 326 | isCoop = true | ||
| 327 | } | ||
| 328 | |||
| 329 | current_chapter = data.map.chapter_name.split(" ") | ||
| 330 | // current_chapter = current_chapter.split("-") | ||
| 331 | current_chapter = current_chapter[1] | ||
| 332 | |||
| 333 | return ( | ||
| 334 | <> | ||
| 335 | {token!==null?mod===true?<Modview selectedRun={selectedRun} data={data} token={token}/>:"":""} | ||
| 336 | |||
| 337 | <div id='background-image'> | ||
| 338 | <img src={data.map.image} alt="" /> | ||
| 339 | </div> | ||
| 340 | <main> | ||
| 341 | <section id='section1' className='summary1'> | ||
| 342 | <div> | ||
| 343 | <Link to="/games"><button className='nav-button' style={{borderRadius: "20px 0px 0px 20px"}}><i className='triangle'></i><span>Games list</span></button></Link> | ||
| 344 | <Link to={`/games/${!data.map.is_coop ? "1" : "2"}?chapter=${current_chapter}`}><button className='nav-button' style={{borderRadius: "0px 20px 20px 0px", marginLeft: "2px"}}><i className='triangle'></i><span>{data.map.chapter_name}</span></button></Link> | ||
| 345 | <br/><span><b>{data.map.map_name}</b></span> | ||
| 346 | </div> | ||
| 347 | |||
| 348 | |||
| 349 | </section> | ||
| 350 | |||
| 351 | <section id='section2' className='summary1'> | ||
| 352 | <button className='nav-button' onClick={()=>setNavState(0)}><img src={img4} alt="" /><span>Summary</span></button> | ||
| 353 | <button className='nav-button' onClick={()=>setNavState(1)}><img src={img5} alt="" /><span>Leaderboards</span></button> | ||
| 354 | <button className='nav-button' onClick={()=>setNavState(2)}><img src={img6} alt="" /><span>Discussions</span></button> | ||
| 355 | </section> | ||
| 356 | <section id='section3' className='summary1'> | ||
| 357 | <div id='category' | ||
| 358 | style={data.map.image===""?{backgroundColor:"#202232"}:{}}> | ||
| 359 | <img src={data.map.image} alt="" id='category-image'></img> | ||
| 360 | <p><span className='portal-count'>{data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].history.score_count}</span> | ||
| 361 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].history.score_count === 1 ? ` portal` : ` portals` }</p> | ||
| 362 | <span> | ||
| 363 | <button onClick={()=>setCatState(1)}>CM</button> | ||
| 364 | <button onClick={()=>setCatState(2)}>NoSLA</button> | ||
| 365 | {data.map.is_coop?<button onClick={()=>setCatState(3)}>SLA</button> | ||
| 366 | :<button onClick={()=>setCatState(3)}>Inbounds SLA</button>} | ||
| 367 | <button onClick={()=>setCatState(4)}>Any%</button> | ||
| 368 | </span> | ||
| 369 | |||
| 370 | </div> | ||
| 371 | |||
| 372 | <div id='history'> | ||
| 373 | |||
| 374 | <div style={{display: hisState ? "none" : "block"}}> | ||
| 375 | {data.summary.routes.filter(e=>e.category.id===catState).length===0 ? <h5>There are no records for this map.</h5> : | ||
| 376 | <> | ||
| 377 | <div className='record-top'> | ||
| 378 | <span>Date</span> | ||
| 379 | <span>Record</span> | ||
| 380 | <span>First completion</span> | ||
| 381 | </div> | ||
| 382 | <hr/> | ||
| 383 | <div id='records'> | ||
| 384 | |||
| 385 | {data.summary.routes | ||
| 386 | .sort((a, b) => a.history.score_count - b.history.score_count) | ||
| 387 | .filter(e=>e.category.id===catState) | ||
| 388 | .map((r, index) => ( | ||
| 389 | <button className='record' key={index} onClick={()=>{ | ||
| 390 | selectRun(index,r.category.id); | ||
| 391 | }}> | ||
| 392 | <span>{ new Date(r.history.date).toLocaleDateString( | ||
| 393 | "en-US", { month: 'long', day: 'numeric', year: 'numeric' } | ||
| 394 | )}</span> | ||
| 395 | <span>{r.history.score_count}</span> | ||
| 396 | <span>{r.history.runner_name}</span> | ||
| 397 | </button> | ||
| 398 | ))} | ||
| 399 | </div> | ||
| 400 | </> | ||
| 401 | } | ||
| 402 | </div> | ||
| 403 | |||
| 404 | <div style={{display: hisState ? "block" : "none"}}> | ||
| 405 | {data.summary.routes.filter(e=>e.category.id===catState).length===0 ? <h5>There are no records for this map.</h5> : | ||
| 406 | <div id='graph'> | ||
| 407 | <div>{graph(1)}</div> | ||
| 408 | <div>{graph(2)}</div> | ||
| 409 | <div>{graph(3)}</div> | ||
| 410 | </div> | ||
| 411 | } | ||
| 412 | </div> | ||
| 413 | <span> | ||
| 414 | <button onClick={()=>setHisState(0)}>List</button> | ||
| 415 | <button onClick={()=>setHisState(1)}>Graph</button> | ||
| 416 | </span> | ||
| 417 | </div> | ||
| 418 | |||
| 419 | |||
| 420 | </section> | ||
| 421 | <section id='section4' className='summary1'> | ||
| 422 | <div id='difficulty'> | ||
| 423 | <span>Difficulty</span> | ||
| 424 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].rating === 0 ? (<span>N/A</span>):null} | ||
| 425 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].rating === 1 ? (<span style={{color:"lime"}}>Very easy</span>):null} | ||
| 426 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].rating === 2 ? (<span style={{color:"green"}}>Easy</span>):null} | ||
| 427 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].rating === 3 ? (<span style={{color:"yellow"}}>Medium</span>):null} | ||
| 428 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].rating === 4 ? (<span style={{color:"orange"}}>Hard</span>):null} | ||
| 429 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].rating === 5 ? (<span style={{color:"red"}}>Very hard</span>):null} | ||
| 430 | <div> | ||
| 431 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].rating === 1 ? (<div className='difficulty-rating' style={{backgroundColor:"lime"}}></div>) : (<div className='difficulty-rating'></div>)} | ||
| 432 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].rating === 2 ? (<div className='difficulty-rating' style={{backgroundColor:"green"}}></div>) : (<div className='difficulty-rating'></div>)} | ||
| 433 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].rating === 3 ? (<div className='difficulty-rating' style={{backgroundColor:"yellow"}}></div>) : (<div className='difficulty-rating'></div>)} | ||
| 434 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].rating === 4 ? (<div className='difficulty-rating' style={{backgroundColor:"orange"}}></div>) : (<div className='difficulty-rating'></div>)} | ||
| 435 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].rating === 5 ? (<div className='difficulty-rating' style={{backgroundColor:"red"}}></div>) : (<div className='difficulty-rating'></div>)} | ||
| 436 | </div> | ||
| 437 | </div> | ||
| 438 | <div id='count'> | ||
| 439 | <span>Completion count</span> | ||
| 440 | <div>{catState===1?data.summary.routes[selectedRun].completion_count:"N/A"}</div> | ||
| 441 | </div> | ||
| 442 | </section> | ||
| 443 | |||
| 444 | <section id='section5' className='summary1'> | ||
| 445 | <div id='description'> | ||
| 446 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].showcase!=="" ? | ||
| 447 | <iframe title='Showcase video' src={vid}> </iframe> | ||
| 448 | : ""} | ||
| 449 | <h3>Route description</h3> | ||
| 450 | <span id='description-text'> | ||
| 451 | <ReactMarkdown> | ||
| 452 | {data.summary.routes.sort((a,b)=>a.category.id - b.category.id)[selectedRun].description} | ||
| 453 | </ReactMarkdown> | ||
| 454 | </span> | ||
| 455 | </div> | ||
| 456 | </section> | ||
| 457 | |||
| 458 | {/* Leaderboards */} | ||
| 459 | |||
| 460 | {lbData===null?"":lbData.success===false?( | ||
| 461 | <section id='section6' className='summary2'> | ||
| 462 | <h1 style={{textAlign:"center"}}>Map is not available for competitive boards.</h1> | ||
| 463 | </section> | ||
| 464 | ):lbData.data.records.length===0?( | ||
| 465 | <section id='section6' className='summary2'> | ||
| 466 | <h1 style={{textAlign:"center"}}>No records found.</h1> | ||
| 467 | </section> | ||
| 468 | ):( | ||
| 469 | <section id='section6' className='summary2'> | ||
| 470 | |||
| 471 | <div id='leaderboard-top' | ||
| 472 | style={lbData.data.map.is_coop?{gridTemplateColumns:"7.5% 40% 7.5% 15% 15% 15%"}:{gridTemplateColumns:"7.5% 30% 10% 20% 17.5% 15%"}} | ||
| 473 | > | ||
| 474 | <span>Place</span> | ||
| 475 | |||
| 476 | {lbData.data.map.is_coop?( | ||
| 477 | <div id='runner'> | ||
| 478 | <span>Host</span> | ||
| 479 | <span>Partner</span> | ||
| 480 | </div> | ||
| 481 | ):( | ||
| 482 | <span>Runner</span> | ||
| 483 | )} | ||
| 484 | |||
| 485 | <span>Portals</span> | ||
| 486 | <span>Time</span> | ||
| 487 | <span>Date</span> | ||
| 488 | <div id='page-number'> | ||
| 489 | <div> | ||
| 490 | |||
| 491 | <button onClick={() => pageNumber === 1 ? null : setPageNumber(prevPageNumber => prevPageNumber - 1)} | ||
| 492 | ><i className='triangle' style={{position:'relative',left:'-5px',}}></i> </button> | ||
| 493 | <span>{lbData.data.pagination.current_page}/{lbData.data.pagination.total_pages}</span> | ||
| 494 | <button onClick={() => pageNumber === lbData.data.pagination.total_pages ? null : setPageNumber(prevPageNumber => prevPageNumber + 1)} | ||
| 495 | ><i className='triangle' style={{position:'relative',left:'5px',transform:'rotate(180deg)'}}></i> </button> | ||
| 496 | </div> | ||
| 497 | </div> | ||
| 498 | </div> | ||
| 499 | <hr/> | ||
| 500 | <div id='leaderboard-records'> | ||
| 501 | {lbData.data.records.map((r, index) => ( | ||
| 502 | <span className='leaderboard-record' key={index} | ||
| 503 | style={lbData.data.map.is_coop?{gridTemplateColumns:"3% 4.5% 40% 4% 3.5% 15% 15% 14.5%"}:{gridTemplateColumns:"3% 4.5% 30% 4% 6% 20% 17% 15%"}} | ||
| 504 | > | ||
| 505 | <span>{r.placement}</span> | ||
| 506 | <span> </span> | ||
| 507 | {lbData.data.map.is_coop?( | ||
| 508 | <div> | ||
| 509 | <span><img src={r.host.avatar_link} alt='' /> {r.host.user_name}</span> | ||
| 510 | <span><img src={r.partner.avatar_link} alt='' /> {r.partner.user_name}</span> | ||
| 511 | </div> | ||
| 512 | ):( | ||
| 513 | <div><span><img src={r.user.avatar_link} alt='' /> {r.user.user_name}</span></div> | ||
| 514 | )} | ||
| 515 | |||
| 516 | <span>{r.score_count}</span> | ||
| 517 | <span> </span> | ||
| 518 | <span>{TicksToTime(r.score_time)}</span> | ||
| 519 | <span className='hover-popup' popup-text={r.record_date.replace("T",' ').split(".")[0]}>{ TimeAgo(new Date(r.record_date.replace("T"," ").replace("Z",""))) }</span> | ||
| 520 | |||
| 521 | {lbData.data.map.is_coop?( | ||
| 522 | <span> | ||
| 523 | <button onClick={()=>{window.alert(`Host demo ID: ${r.host_demo_id} \nParnter demo ID: ${r.partner_demo_id}`)}}><img src={img13} alt="demo_id" /></button> | ||
| 524 | <button onClick={()=>window.location.href=`https://lp.ardapektezol.com/api/v1/demos?uuid=${r.partner_demo_id}`}><img src={img12} alt="download" style={{filter:"hue-rotate(160deg) contrast(60%) saturate(1000%)"}}/></button> | ||
| 525 | <button onClick={()=>window.location.href=`https://lp.ardapektezol.com/api/v1/demos?uuid=${r.host_demo_id}`}><img src={img12} alt="download" style={{filter:"hue-rotate(300deg) contrast(60%) saturate(1000%)"}}/></button> | ||
| 526 | </span> | ||
| 527 | ):( | ||
| 528 | |||
| 529 | <span> | ||
| 530 | <button onClick={()=>{window.alert(`Demo ID: ${r.demo_id}`)}}><img src={img13} alt="demo_id" /></button> | ||
| 531 | <button onClick={()=>window.location.href=`https://lp.ardapektezol.com/api/v1/demos?uuid=${r.demo_id}`}><img src={img12} alt="download" /></button> | ||
| 532 | </span> | ||
| 533 | )} | ||
| 534 | </span> | ||
| 535 | ))} | ||
| 536 | </div> | ||
| 537 | </section> | ||
| 538 | )} | ||
| 539 | |||
| 540 | |||
| 541 | {/* Discussions */} | ||
| 542 | <section id='section7' className='summary3'> | ||
| 543 | |||
| 544 | {discussionThread === null ? ( | ||
| 545 | createPostState === 0 ? ( | ||
| 546 | discussions !== null ? ( | ||
| 547 | // Main screen | ||
| 548 | <> | ||
| 549 | <div id='discussion-search'> | ||
| 550 | <input type="text" value={discussionSearch} placeholder={"Search for posts..."} onChange={()=>setDiscussionSearch(document.querySelector("#discussion-search>input").value)} /> | ||
| 551 | <div><button onClick={()=>setCreatePostState(1)}>New Post</button></div> | ||
| 552 | </div> | ||
| 553 | {discussions.filter(f=>f.title.includes(discussionSearch)).sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)) | ||
| 554 | .map((e, i) => ( | ||
| 555 | <div id='discussion-post'> | ||
| 556 | |||
| 557 | <button key={e.id} onClick={() => openDiscussion(e.id)}> | ||
| 558 | <span>{e.title}</span> | ||
| 559 | |||
| 560 | {token!==null?e.creator.steam_id===JSON.parse(atob(token.split(".")[1])).sub? | ||
| 561 | <button onClick={()=>DeletePost(e)}>Delete Post</button> | ||
| 562 | :<span></span>:<span></span>} | ||
| 563 | <span><b>{e.creator.user_name}:</b> {e.content}</span> | ||
| 564 | <span>last updated: {TimeAgo(new Date(e.updated_at.replace("T"," ").replace("Z","")))}</span> | ||
| 565 | </button> | ||
| 566 | </div> | ||
| 567 | ))} | ||
| 568 | </> | ||
| 569 | ):( | ||
| 570 | |||
| 571 | // Main screen (no posts) | ||
| 572 | <> | ||
| 573 | <div id='discussion-search'> | ||
| 574 | <input type="text" value={discussionSearch} placeholder={"Search for posts..."} onChange={()=>setDiscussionSearch(document.querySelector("#discussion-search>input").value)} /> | ||
| 575 | <div><button onClick={()=>setCreatePostState(1)}>New Post</button></div> | ||
| 576 | </div> | ||
| 577 | <span style={{textAlign:"center",display:"block"}}>no discussions</span> | ||
| 578 | </> | ||
| 579 | ) | ||
| 580 | ):( | ||
| 581 | // Creating post | ||
| 582 | <div id='discussion-create'> | ||
| 583 | <span>Create post</span> | ||
| 584 | <button onClick={()=>setCreatePostState(0)}>X</button> | ||
| 585 | <div style={{gridColumn:"1 / span 2"}}> | ||
| 586 | <input id='discussion-create-title' placeholder='Title...'></input> | ||
| 587 | <input id='discussion-create-content' placeholder='Enter the comment...' ></input> | ||
| 588 | </div> | ||
| 589 | <div style={{placeItems:"end",gridColumn:"1 / span 2"}}> | ||
| 590 | <button id='discussion-create-button' onClick={()=>CreatePost()}>Post</button> | ||
| 591 | </div> | ||
| 592 | |||
| 593 | </div> | ||
| 594 | |||
| 595 | )):( | ||
| 596 | // Post screen | ||
| 597 | <div id='discussion-thread'> | ||
| 598 | <div> | ||
| 599 | <span>{discussionThread.title}</span> | ||
| 600 | <button onClick={()=>setDiscussionThread(null)}>X</button> | ||
| 601 | </div> | ||
| 602 | |||
| 603 | <div> | ||
| 604 | <img src={discussionThread.creator.avatar_link} alt="" /> | ||
| 605 | <div> | ||
| 606 | <span>{discussionThread.creator.user_name}</span> | ||
| 607 | <span>{TimeAgo(new Date(discussionThread.created_at.replace("T"," ").replace("Z","")))}</span> | ||
| 608 | <span>{discussionThread.content}</span> | ||
| 609 | </div> | ||
| 610 | {discussionThread.comments!==null? | ||
| 611 | discussionThread.comments.sort((a, b) => new Date(a.date) - new Date(b.date)) | ||
| 612 | .map(e=>( | ||
| 613 | <> | ||
| 614 | <img src={e.user.avatar_link} alt="" /> | ||
| 615 | <div> | ||
| 616 | <span>{e.user.user_name}</span> | ||
| 617 | <span>{TimeAgo(new Date(e.date.replace("T"," ").replace("Z","")))}</span> | ||
| 618 | <span>{e.comment}</span> | ||
| 619 | </div> | ||
| 620 | </> | ||
| 621 | |||
| 622 | )):""} | ||
| 623 | |||
| 624 | |||
| 625 | </div> | ||
| 626 | <div id='discussion-send'> | ||
| 627 | <input type="text" placeholder={"Message"} onKeyDown={(e)=>e.key==="Enter"?PostComment():""}/> | ||
| 628 | <div><button onClick={()=>PostComment()}>Send</button></div> | ||
| 629 | </div> | ||
| 630 | |||
| 631 | </div> | ||
| 632 | |||
| 633 | |||
| 634 | )} | ||
| 635 | |||
| 636 | </section> | ||
| 637 | |||
| 638 | </main> | ||
| 639 | </> | ||
| 640 | ) | ||
| 641 | }else{ | ||
| 642 | return ( | ||
| 643 | <main></main> | ||
| 644 | ) | ||
| 645 | } | ||
| 646 | |||
| 647 | |||
| 648 | } | ||
| 649 | |||
| 650 | |||
diff --git a/frontend/src/components/pages/summary_modview.js b/frontend/src/components/pages/summary_modview.js deleted file mode 100644 index 3541c48..0000000 --- a/frontend/src/components/pages/summary_modview.js +++ /dev/null | |||
| @@ -1,254 +0,0 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { useLocation } from "react-router-dom"; | ||
| 3 | import ReactMarkdown from 'react-markdown' | ||
| 4 | |||
| 5 | import "./summary_modview.css"; | ||
| 6 | |||
| 7 | |||
| 8 | export default function Modview(prop) { | ||
| 9 | const {selectedRun,data,token} = prop | ||
| 10 | |||
| 11 | const [menu,setMenu] = React.useState(0) | ||
| 12 | React.useEffect(()=>{ | ||
| 13 | if(menu===3){ // add | ||
| 14 | document.querySelector("#modview-route-name>input").value="" | ||
| 15 | document.querySelector("#modview-route-score>input").value="" | ||
| 16 | document.querySelector("#modview-route-date>input").value="" | ||
| 17 | document.querySelector("#modview-route-showcase>input").value="" | ||
| 18 | document.querySelector("#modview-route-description>textarea").value="" | ||
| 19 | } | ||
| 20 | if(menu===2){ // edit | ||
| 21 | document.querySelector("#modview-route-id>input").value=data.summary.routes[selectedRun].route_id | ||
| 22 | document.querySelector("#modview-route-name>input").value=data.summary.routes[selectedRun].history.runner_name | ||
| 23 | document.querySelector("#modview-route-score>input").value=data.summary.routes[selectedRun].history.score_count | ||
| 24 | document.querySelector("#modview-route-date>input").value=data.summary.routes[selectedRun].history.date.split("T")[0] | ||
| 25 | document.querySelector("#modview-route-showcase>input").value=data.summary.routes[selectedRun].showcase | ||
| 26 | document.querySelector("#modview-route-description>textarea").value=data.summary.routes[selectedRun].description | ||
| 27 | } // eslint-disable-next-line | ||
| 28 | },[menu]) | ||
| 29 | |||
| 30 | function compressImage(file) { | ||
| 31 | const reader = new FileReader(); | ||
| 32 | reader.readAsDataURL(file); | ||
| 33 | return new Promise(resolve => { | ||
| 34 | reader.onload = () => { | ||
| 35 | const img = new Image(); | ||
| 36 | img.src = reader.result; | ||
| 37 | img.onload = () => { | ||
| 38 | let {width, height} = img; | ||
| 39 | if (width > 550) { | ||
| 40 | height *= 550 / width; | ||
| 41 | width = 550; | ||
| 42 | } | ||
| 43 | if (height > 320) { | ||
| 44 | width *= 320 / height; | ||
| 45 | height = 320; | ||
| 46 | } | ||
| 47 | const canvas = document.createElement('canvas'); | ||
| 48 | canvas.width = width; | ||
| 49 | canvas.height = height; | ||
| 50 | canvas.getContext('2d').drawImage(img, 0, 0, width, height); | ||
| 51 | resolve(canvas.toDataURL(file.type, 0.6)); | ||
| 52 | }; | ||
| 53 | }; | ||
| 54 | }); | ||
| 55 | } | ||
| 56 | const [image,setImage] = React.useState(null) | ||
| 57 | function uploadImage(){ | ||
| 58 | if(window.confirm("Are you sure you want to submit this to the database?")){ | ||
| 59 | fetch(`/api/v1/maps/${location.pathname.split('/')[2]}/image`,{ | ||
| 60 | method: 'PUT', | ||
| 61 | headers: {Authorization: token}, | ||
| 62 | body: JSON.stringify({"image": image}) | ||
| 63 | }).then(r=>window.location.reload()) | ||
| 64 | } | ||
| 65 | } | ||
| 66 | const location = useLocation() | ||
| 67 | function editRoute(){ | ||
| 68 | if(window.confirm("Are you sure you want to submit this to the database?")){ | ||
| 69 | let payload = { | ||
| 70 | "description": document.querySelector("#modview-route-description>textarea").value===""?"No description available.":document.querySelector("#modview-route-description>textarea").value, | ||
| 71 | "record_date": document.querySelector("#modview-route-date>input").value+"T00:00:00Z", | ||
| 72 | "route_id": parseInt(document.querySelector("#modview-route-id>input").value), | ||
| 73 | "score_count": parseInt(document.querySelector("#modview-route-score>input").value), | ||
| 74 | "showcase": document.querySelector("#modview-route-showcase>input").value, | ||
| 75 | "user_name": document.querySelector("#modview-route-name>input").value | ||
| 76 | } | ||
| 77 | fetch(`/api/v1/maps/${location.pathname.split('/')[2]}/summary`,{ | ||
| 78 | method: 'PUT', | ||
| 79 | headers: {Authorization: token}, | ||
| 80 | body: JSON.stringify(payload) | ||
| 81 | }).then(r=>window.location.reload()) | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | |||
| 86 | function addRoute(){ | ||
| 87 | if(window.confirm("Are you sure you want to submit this to the database?")){ | ||
| 88 | let payload = { | ||
| 89 | "category_id": parseInt(document.querySelector("#modview-route-category>select").value), | ||
| 90 | "description": document.querySelector("#modview-route-description>textarea").value===""?"No description available.":document.querySelector("#modview-route-description>textarea").value, | ||
| 91 | "record_date": document.querySelector("#modview-route-date>input").value+"T00:00:00Z", | ||
| 92 | "score_count": parseInt(document.querySelector("#modview-route-score>input").value), | ||
| 93 | "showcase": document.querySelector("#modview-route-showcase>input").value, | ||
| 94 | "user_name": document.querySelector("#modview-route-name>input").value | ||
| 95 | } | ||
| 96 | fetch(`/api/v1/maps/${location.pathname.split('/')[2]}/summary`,{ | ||
| 97 | method: 'POST', | ||
| 98 | headers: {Authorization: token}, | ||
| 99 | body: JSON.stringify(payload) | ||
| 100 | }).then(r=>window.location.reload()) | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | function deleteRoute(){ | ||
| 105 | if(data.summary.routes[0].category==='') | ||
| 106 | {window.alert("no run selected")}else{ | ||
| 107 | if(window.confirm(`Are you sure you want to delete this run from the database? | ||
| 108 | ${data.summary.routes[selectedRun].category.name} ${data.summary.routes[selectedRun].history.score_count} portals ${data.summary.routes[selectedRun].history.runner_name}`)===true){ | ||
| 109 | console.log("deleted:",selectedRun) | ||
| 110 | fetch(`/api/v1/maps/${location.pathname.split('/')[2]}/summary`,{ | ||
| 111 | method: 'DELETE', | ||
| 112 | headers: {Authorization: token}, | ||
| 113 | body: JSON.stringify({"route_id":data.summary.routes[selectedRun].route_id}) | ||
| 114 | }).then(r=>window.location.reload()) | ||
| 115 | }} | ||
| 116 | |||
| 117 | } | ||
| 118 | |||
| 119 | const [showButton, setShowButton] = React.useState(1) | ||
| 120 | const modview = document.querySelector("div#modview") | ||
| 121 | React.useEffect(()=>{ | ||
| 122 | if(modview!==null){ | ||
| 123 | showButton ? modview.style.transform="translateY(-68%)" | ||
| 124 | : modview.style.transform="translateY(0%)" | ||
| 125 | } | ||
| 126 | let modview_block = document.querySelector("#modview_block") | ||
| 127 | showButton===1?modview_block.style.display="none":modview_block.style.display="block"// eslint-disable-next-line | ||
| 128 | },[showButton]) | ||
| 129 | |||
| 130 | const [md,setMd] = React.useState("") | ||
| 131 | |||
| 132 | return ( | ||
| 133 | <> | ||
| 134 | <div id="modview_block"></div> | ||
| 135 | <div id='modview'> | ||
| 136 | <div> | ||
| 137 | <button onClick={()=>setMenu(1)}>edit image</button> | ||
| 138 | <button onClick={ | ||
| 139 | data.summary.routes[0].category===''?()=>window.alert("no run selected"):()=>setMenu(2)}>edit selected route</button> | ||
| 140 | <button onClick={()=>setMenu(3)}>add new route</button> | ||
| 141 | <button onClick={()=>deleteRoute()}>delete selected route</button> | ||
| 142 | </div> | ||
| 143 | <div> | ||
| 144 | {showButton ?( | ||
| 145 | <button onClick={()=>setShowButton(0)}>Show</button> | ||
| 146 | ) : ( | ||
| 147 | <button onClick={()=>{setShowButton(1);setMenu(0)}}>Hide</button> | ||
| 148 | )} | ||
| 149 | </div> | ||
| 150 | </div> | ||
| 151 | {menu!==0? ( | ||
| 152 | <div id='modview-menu'> | ||
| 153 | {menu===1? ( | ||
| 154 | // image | ||
| 155 | <div id='modview-menu-image'> | ||
| 156 | <div> | ||
| 157 | <span>current image:</span> | ||
| 158 | <img src={data.map.image} alt="missing" /> | ||
| 159 | </div> | ||
| 160 | |||
| 161 | <div> | ||
| 162 | <span>new image: | ||
| 163 | <input type="file" accept='image/*' onChange={e=> | ||
| 164 | compressImage(e.target.files[0]) | ||
| 165 | .then(d=>setImage(d)) | ||
| 166 | }/></span> | ||
| 167 | {image!==null?(<button onClick={()=>uploadImage()}>upload</button>):<span></span>} | ||
| 168 | <img src={image} alt="" id='modview-menu-image-file'/> | ||
| 169 | |||
| 170 | </div> | ||
| 171 | </div> | ||
| 172 | ):menu===2?( | ||
| 173 | // edit route | ||
| 174 | <div id='modview-menu-edit'> | ||
| 175 | <div id='modview-route-id'> | ||
| 176 | <span>route id:</span> | ||
| 177 | <input type="number" disabled/> | ||
| 178 | </div> | ||
| 179 | <div id='modview-route-name'> | ||
| 180 | <span>runner name:</span> | ||
| 181 | <input type="text"/> | ||
| 182 | </div> | ||
| 183 | <div id='modview-route-score'> | ||
| 184 | <span>score:</span> | ||
| 185 | <input type="number"/> | ||
| 186 | </div> | ||
| 187 | <div id='modview-route-date'> | ||
| 188 | <span>date:</span> | ||
| 189 | <input type="date"/> | ||
| 190 | </div> | ||
| 191 | <div id='modview-route-showcase'> | ||
| 192 | <span>showcase video:</span> | ||
| 193 | <input type="text"/> | ||
| 194 | </div> | ||
| 195 | <div id='modview-route-description' style={{height:"180px",gridColumn:"1 / span 5"}}> | ||
| 196 | <span>description:</span> | ||
| 197 | <textarea onChange={()=>setMd(document.querySelector("#modview-route-description>textarea").value)}></textarea> | ||
| 198 | </div> | ||
| 199 | <button style={{gridColumn:"2 / span 3",height:"40px"}} onClick={editRoute}>Apply</button> | ||
| 200 | </div> | ||
| 201 | ):menu===3?( | ||
| 202 | // add route | ||
| 203 | <div id='modview-menu-add'> | ||
| 204 | <div id='modview-route-category'> | ||
| 205 | <span>category:</span> | ||
| 206 | <select> | ||
| 207 | <option value="1" key="1">CM</option> | ||
| 208 | <option value="2" key="2">No SLA</option> | ||
| 209 | {data.map.game_name==="Portal 2 - Cooperative"?"":( | ||
| 210 | <option value="3" key="3">Inbounds SLA</option>)} | ||
| 211 | <option value="4" key="4">Any%</option> | ||
| 212 | </select> | ||
| 213 | </div> | ||
| 214 | <div id='modview-route-name'> | ||
| 215 | <span>runner name:</span> | ||
| 216 | <input type="text" /> | ||
| 217 | </div> | ||
| 218 | <div id='modview-route-score'> | ||
| 219 | <span>score:</span> | ||
| 220 | <input type="number" /> | ||
| 221 | </div> | ||
| 222 | <div id='modview-route-date'> | ||
| 223 | <span>date:</span> | ||
| 224 | <input type="date" /> | ||
| 225 | </div> | ||
| 226 | <div id='modview-route-showcase'> | ||
| 227 | <span>showcase video:</span> | ||
| 228 | <input type="text" /> | ||
| 229 | </div> | ||
| 230 | <div id='modview-route-description' style={{height:"180px",gridColumn:"1 / span 5"}}> | ||
| 231 | <span>description:</span> | ||
| 232 | <textarea defaultValue={"No description available."} onChange={()=>setMd(document.querySelector("#modview-route-description>textarea").value)}></textarea> | ||
| 233 | </div> | ||
| 234 | <button style={{gridColumn:"2 / span 3",height:"40px"}} onClick={addRoute}>Apply</button> | ||
| 235 | </div> | ||
| 236 | ):("error")} | ||
| 237 | |||
| 238 | {menu!==1?( | ||
| 239 | <div id='modview-md'> | ||
| 240 | <span>Markdown preview</span> | ||
| 241 | <span><a href="https://commonmark.org/help/" rel="noreferrer" target='_blank'>documentation</a></span> | ||
| 242 | <span><a href="https://remarkjs.github.io/react-markdown/" rel="noreferrer" target='_blank'>demo</a></span> | ||
| 243 | <p> | ||
| 244 | <ReactMarkdown>{md} | ||
| 245 | </ReactMarkdown> | ||
| 246 | </p> | ||
| 247 | </div> | ||
| 248 | ):""} | ||
| 249 | </div>):""} | ||
| 250 | |||
| 251 | </> | ||
| 252 | ) | ||
| 253 | } | ||
| 254 | |||
diff --git a/frontend/src/components/record.css b/frontend/src/components/record.css deleted file mode 100644 index 60d47ee..0000000 --- a/frontend/src/components/record.css +++ /dev/null | |||
| @@ -1,15 +0,0 @@ | |||
| 1 | .record-container { | ||
| 2 | --padding: 20px; | ||
| 3 | width: calc(100% - calc(var(--padding * 2))); | ||
| 4 | height: 42px; | ||
| 5 | background-color: #2B2E46; | ||
| 6 | border-radius: 200px; | ||
| 7 | font-size: 18px; | ||
| 8 | display: grid; | ||
| 9 | grid-template-columns: 20% 25% 15% 15% 25%; | ||
| 10 | text-align: center; | ||
| 11 | padding: 0px var(--padding); | ||
| 12 | vertical-align: middle; | ||
| 13 | align-items: center; | ||
| 14 | margin-bottom: 6px; | ||
| 15 | } \ No newline at end of file | ||
diff --git a/frontend/src/components/record.js b/frontend/src/components/record.js deleted file mode 100644 index 80e084d..0000000 --- a/frontend/src/components/record.js +++ /dev/null | |||
| @@ -1,56 +0,0 @@ | |||
| 1 | import React, { useEffect, useRef, useState } from 'react'; | ||
| 2 | import { useLocation, Link } from "react-router-dom"; | ||
| 3 | |||
| 4 | import "./record.css" | ||
| 5 | |||
| 6 | export default function Record({ name, place, portals, time, date }) { | ||
| 7 | // const {token} = prop; | ||
| 8 | const [record, setRecord] = useState(null); | ||
| 9 | const location = useLocation(); | ||
| 10 | |||
| 11 | // useEffect(() => { | ||
| 12 | // console.log(name, place, portals, time, date); | ||
| 13 | // }) | ||
| 14 | |||
| 15 | function timeSince() { | ||
| 16 | const now = new Date(); | ||
| 17 | const dateNew = new Date(date); | ||
| 18 | |||
| 19 | const secondsPast = Math.floor((now - dateNew) / 1000); | ||
| 20 | |||
| 21 | if (secondsPast < 60) { | ||
| 22 | return `${secondsPast} seconds ago`; | ||
| 23 | } | ||
| 24 | if (secondsPast < 3600) { | ||
| 25 | const minutes = Math.floor(secondsPast / 60); | ||
| 26 | return `${minutes} minutes ago`; | ||
| 27 | } | ||
| 28 | if (secondsPast < 86400) { | ||
| 29 | const hours = Math.floor(secondsPast / 3600); | ||
| 30 | return `${hours} hours ago`; | ||
| 31 | } | ||
| 32 | if (secondsPast < 2592000) { | ||
| 33 | const days = Math.floor(secondsPast / 86400); | ||
| 34 | return `${days} days ago`; | ||
| 35 | } | ||
| 36 | if (secondsPast < 31536000) { | ||
| 37 | const months = Math.floor(secondsPast / 2592000); | ||
| 38 | return `${months} months ago`; | ||
| 39 | } | ||
| 40 | const years = Math.floor(secondsPast / 31536000); | ||
| 41 | return `${years} years ago`; | ||
| 42 | } | ||
| 43 | |||
| 44 | return( | ||
| 45 | <div className='record-container'> | ||
| 46 | <span>{place}</span> | ||
| 47 | <div style={{display: "flex", alignItems: "center"}}> | ||
| 48 | <img style={{height: "40px", borderRadius: "200px"}} src="https://avatars.steamstatic.com/32d110951da2339d8b8d8419bc945d9a2b150b2a_full.jpg"></img> | ||
| 49 | <span style={{paddingLeft: "5px", fontFamily: "BarlowSemiCondensed-SemiBold"}}>{name}</span> | ||
| 50 | </div> | ||
| 51 | <span style={{fontFamily: "BarlowCondensed-Bold", color: "#D980FF"}}>{portals}</span> | ||
| 52 | <span>{time}</span> | ||
| 53 | <span>{timeSince()}</span> | ||
| 54 | </div> | ||
| 55 | ) | ||
| 56 | } | ||
diff --git a/frontend/src/components/sidebar.js b/frontend/src/components/sidebar.js deleted file mode 100644 index 1ca17e6..0000000 --- a/frontend/src/components/sidebar.js +++ /dev/null | |||
| @@ -1,203 +0,0 @@ | |||
| 1 | import React, { useEffect } from 'react'; | ||
| 2 | import { Link, useLocation } from "react-router-dom"; | ||
| 3 | |||
| 4 | import "../App.css" | ||
| 5 | import "./sidebar.css"; | ||
| 6 | import logo from "../imgs/logo.png" | ||
| 7 | import img1 from "../imgs/1.png" | ||
| 8 | import img2 from "../imgs/2.png" | ||
| 9 | import img3 from "../imgs/3.png" | ||
| 10 | import img4 from "../imgs/4.png" | ||
| 11 | import img5 from "../imgs/5.png" | ||
| 12 | import img6 from "../imgs/6.png" | ||
| 13 | import img7 from "../imgs/7.png" | ||
| 14 | import img8 from "../imgs/8.png" | ||
| 15 | import img9 from "../imgs/9.png" | ||
| 16 | import Login from "./login.js" | ||
| 17 | |||
| 18 | export default function Sidebar(prop) { | ||
| 19 | const {token,setToken} = prop | ||
| 20 | const [profile, setProfile] = React.useState(null); | ||
| 21 | |||
| 22 | React.useEffect(() => { | ||
| 23 | fetch(`https://lp.ardapektezol.com/api/v1/profile`,{ | ||
| 24 | headers: { | ||
| 25 | 'Content-Type': 'application/json', | ||
| 26 | Authorization: token | ||
| 27 | }}) | ||
| 28 | .then(r => r.json()) | ||
| 29 | .then(d => setProfile(d.data)) | ||
| 30 | }, [token]); | ||
| 31 | |||
| 32 | // Locks search button for 300ms before it can be clicked again, prevents spam | ||
| 33 | const [isLocked, setIsLocked] = React.useState(false); | ||
| 34 | function HandleLock(arg) { | ||
| 35 | if (!isLocked) { | ||
| 36 | setIsLocked(true); | ||
| 37 | setTimeout(() => setIsLocked(false), 300); | ||
| 38 | SidebarHide(arg) | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | |||
| 43 | // The menu button | ||
| 44 | const [sidebar, setSidebar] = React.useState(); | ||
| 45 | |||
| 46 | // Clicked buttons | ||
| 47 | function SidebarClick(x){ | ||
| 48 | const btn = document.querySelectorAll("button.sidebar-button"); | ||
| 49 | |||
| 50 | if(sidebar===1){setSidebar(0);SidebarHide()} | ||
| 51 | |||
| 52 | // clusterfuck | ||
| 53 | btn.forEach((e,i) =>{ | ||
| 54 | btn[i].classList.remove("sidebar-button-selected") | ||
| 55 | btn[i].classList.add("sidebar-button-deselected") | ||
| 56 | }) | ||
| 57 | btn[x].classList.add("sidebar-button-selected") | ||
| 58 | btn[x].classList.remove("sidebar-button-deselected") | ||
| 59 | |||
| 60 | } | ||
| 61 | |||
| 62 | function SidebarHide(){ | ||
| 63 | const btn = document.querySelectorAll("button.sidebar-button") | ||
| 64 | const span = document.querySelectorAll("button.sidebar-button>span"); | ||
| 65 | const side = document.querySelector("#sidebar-list"); | ||
| 66 | const login = document.querySelectorAll(".login>button")[1]; | ||
| 67 | const searchbar = document.querySelector("#searchbar"); | ||
| 68 | |||
| 69 | if(sidebar===1){ | ||
| 70 | setSidebar(0) | ||
| 71 | side.style.width="320px" | ||
| 72 | btn.forEach((e, i) =>{ | ||
| 73 | e.style.width="310px" | ||
| 74 | e.style.padding = "0.4em 0 0 11px" | ||
| 75 | setTimeout(() => { | ||
| 76 | span[i].style.opacity="1" | ||
| 77 | login.style.opacity="1" | ||
| 78 | |||
| 79 | }, 100) | ||
| 80 | }) | ||
| 81 | side.style.zIndex="2" | ||
| 82 | } else { | ||
| 83 | side.style.width="40px"; | ||
| 84 | searchbar.focus(); | ||
| 85 | setSearch(searchbar.value) | ||
| 86 | setSidebar(1) | ||
| 87 | btn.forEach((e,i) =>{ | ||
| 88 | e.style.width="40px" | ||
| 89 | e.style.padding = "0.4em 0 0 5px" | ||
| 90 | span[i].style.opacity="0" | ||
| 91 | }) | ||
| 92 | login.style.opacity="0" | ||
| 93 | setTimeout(() => { | ||
| 94 | side.style.zIndex="0" | ||
| 95 | }, 300); | ||
| 96 | } | ||
| 97 | } | ||
| 98 | // Links | ||
| 99 | const location = useLocation() | ||
| 100 | React.useEffect(()=>{ | ||
| 101 | if(location.pathname==="/"){SidebarClick(1)} | ||
| 102 | if(location.pathname.includes("news")){SidebarClick(2)} | ||
| 103 | if(location.pathname.includes("games")){SidebarClick(3)} | ||
| 104 | if(location.pathname.includes("leaderboards")){SidebarClick(4)} | ||
| 105 | if(location.pathname.includes("scorelog")){SidebarClick(5)} | ||
| 106 | if(location.pathname.includes("profile")){SidebarClick(6)} | ||
| 107 | if(location.pathname.includes("rules")){SidebarClick(8)} | ||
| 108 | if(location.pathname.includes("about")){SidebarClick(9)} | ||
| 109 | |||
| 110 | // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| 111 | }, [location.pathname]) | ||
| 112 | |||
| 113 | const [search,setSearch] = React.useState(null) | ||
| 114 | const [searchData,setSearchData] = React.useState(null) | ||
| 115 | |||
| 116 | React.useEffect(()=>{ | ||
| 117 | fetch(`https://lp.ardapektezol.com/api/v1/search?q=${search}`) | ||
| 118 | .then(r=>r.json()) | ||
| 119 | .then(d=>setSearchData(d.data)) | ||
| 120 | |||
| 121 | }, [search]) | ||
| 122 | |||
| 123 | |||
| 124 | return ( | ||
| 125 | <div id='sidebar'> | ||
| 126 | <div id='logo'> {/* logo */} | ||
| 127 | <img src={logo} alt="" height={"80px"}/> | ||
| 128 | <div id='logo-text'> | ||
| 129 | <span><b>PORTAL 2</b></span><br/> | ||
| 130 | <span>Least Portals</span> | ||
| 131 | </div> | ||
| 132 | </div> | ||
| 133 | <div id='sidebar-list'> {/* List */} | ||
| 134 | <div id='sidebar-toplist'> {/* Top */} | ||
| 135 | |||
| 136 | <button className='sidebar-button' onClick={()=>HandleLock()}><img src={img1} alt="" /><span>Search</span></button> | ||
| 137 | |||
| 138 | <span></span> | ||
| 139 | |||
| 140 | <Link to="/" tabIndex={-1}> | ||
| 141 | <button className='sidebar-button'><img src={img2} alt="" /><span>Home Page</span></button> | ||
| 142 | </Link> | ||
| 143 | |||
| 144 | <Link to="/news" tabIndex={-1}> | ||
| 145 | <button className='sidebar-button'><img src={img3} alt="" /><span>News</span></button> | ||
| 146 | </Link> | ||
| 147 | |||
| 148 | <Link to="/games" tabIndex={-1}> | ||
| 149 | <button className='sidebar-button'><img src={img4} alt="" /><span>Games</span></button> | ||
| 150 | </Link> | ||
| 151 | |||
| 152 | <Link to="/leaderboards" tabIndex={-1}> | ||
| 153 | <button className='sidebar-button'><img src={img5} alt="" /><span>Leaderboards</span></button> | ||
| 154 | </Link> | ||
| 155 | |||
| 156 | <Link to="/scorelog" tabIndex={-1}> | ||
| 157 | <button className='sidebar-button'><img src={img7} alt="" /><span>Score Logs</span></button> | ||
| 158 | </Link> | ||
| 159 | </div> | ||
| 160 | <div id='sidebar-bottomlist'> | ||
| 161 | <span></span> | ||
| 162 | |||
| 163 | <Login setToken={setToken} profile={profile} setProfile={setProfile}/> | ||
| 164 | |||
| 165 | <Link to="/rules" tabIndex={-1}> | ||
| 166 | <button className='sidebar-button'><img src={img8} alt="" /><span>Leaderboard Rules</span></button> | ||
| 167 | </Link> | ||
| 168 | |||
| 169 | <Link to="/about" tabIndex={-1}> | ||
| 170 | <button className='sidebar-button'><img src={img9} alt="" /><span>About P2LP</span></button> | ||
| 171 | </Link> | ||
| 172 | </div> | ||
| 173 | </div> | ||
| 174 | <div> | ||
| 175 | <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={()=>setSearch(document.querySelector("#searchbar").value)}/> | ||
| 176 | |||
| 177 | <div id='search-data'> | ||
| 178 | |||
| 179 | {searchData!==null?searchData.maps.map((q,index)=>( | ||
| 180 | <Link to={`/maps/${q.id}`} className='search-map' key={index}> | ||
| 181 | <span>{q.game}</span> | ||
| 182 | <span>{q.chapter}</span> | ||
| 183 | <span>{q.map}</span> | ||
| 184 | </Link> | ||
| 185 | )):""} | ||
| 186 | {searchData!==null?searchData.players.map((q,index)=> | ||
| 187 | ( | ||
| 188 | <Link to={ | ||
| 189 | profile!==null&&q.steam_id===profile.steam_id?`/profile`: | ||
| 190 | `/users/${q.steam_id}` | ||
| 191 | } className='search-player' key={index}> | ||
| 192 | <img src={q.avatar_link} alt='pfp'></img> | ||
| 193 | <span style={{fontSize:`${36 - q.user_name.length * 0.8}px`}}>{q.user_name}</span> | ||
| 194 | </Link> | ||
| 195 | )):""} | ||
| 196 | |||
| 197 | </div> | ||
| 198 | </div> | ||
| 199 | </div> | ||
| 200 | ) | ||
| 201 | } | ||
| 202 | |||
| 203 | |||
diff --git a/frontend/src/components/pages/games.css b/frontend/src/css/Games.css index ec57a71..ec57a71 100644 --- a/frontend/src/components/pages/games.css +++ b/frontend/src/css/Games.css | |||
diff --git a/frontend/src/components/login.css b/frontend/src/css/Login.css index b46be10..aa75f98 100644 --- a/frontend/src/components/login.css +++ b/frontend/src/css/Login.css | |||
| @@ -13,7 +13,7 @@ span>img { | |||
| 13 | 13 | ||
| 14 | padding-left: 10px; | 14 | padding-left: 10px; |
| 15 | background-color: #00000000 !important; | 15 | background-color: #00000000 !important; |
| 16 | transition: opacity .1s; | 16 | /* transition: opacity .1s; */ |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | .login{ | 19 | .login{ |
diff --git a/frontend/src/components/pages/summary.css b/frontend/src/css/Maps.css index 8c6ec35..d164d3b 100644 --- a/frontend/src/components/pages/summary.css +++ b/frontend/src/css/Maps.css | |||
| @@ -65,7 +65,7 @@ | |||
| 65 | #section3{ | 65 | #section3{ |
| 66 | margin: 40px 0 0 0; | 66 | margin: 40px 0 0 0; |
| 67 | 67 | ||
| 68 | display: none; | 68 | display: grid; |
| 69 | grid-template-columns: 1fr 1fr; | 69 | grid-template-columns: 1fr 1fr; |
| 70 | gap: 20px; | 70 | gap: 20px; |
| 71 | } | 71 | } |
| @@ -239,7 +239,7 @@ p>span.portal-count{font-weight: bold;font-size: 100px;vertical-align: -15%;} | |||
| 239 | /* Section 4: Difficulty + count */ | 239 | /* Section 4: Difficulty + count */ |
| 240 | 240 | ||
| 241 | #section4{ | 241 | #section4{ |
| 242 | display: none; | 242 | display: grid; |
| 243 | grid-template-columns: 1fr 1fr; | 243 | grid-template-columns: 1fr 1fr; |
| 244 | gap: 20px; | 244 | gap: 20px; |
| 245 | margin: 40px 0 0 0; | 245 | margin: 40px 0 0 0; |
| @@ -424,6 +424,7 @@ text-align: center; | |||
| 424 | 424 | ||
| 425 | .hover-popup { | 425 | .hover-popup { |
| 426 | position: relative; | 426 | position: relative; |
| 427 | display: inline-block; | ||
| 427 | } | 428 | } |
| 428 | 429 | ||
| 429 | .hover-popup::after { | 430 | .hover-popup::after { |
| @@ -438,8 +439,13 @@ text-align: center; | |||
| 438 | border-radius: 8px; | 439 | border-radius: 8px; |
| 439 | visibility: hidden; | 440 | visibility: hidden; |
| 440 | opacity: 0; | 441 | opacity: 0; |
| 442 | color: #cdcfdf; | ||
| 441 | /* transition: visibility 0s, opacity 0.3s ease; */ | 443 | /* transition: visibility 0s, opacity 0.3s ease; */ |
| 442 | } | 444 | } |
| 445 | |||
| 446 | .hover-popup:hover { | ||
| 447 | color: transparent; | ||
| 448 | } | ||
| 443 | 449 | ||
| 444 | .hover-popup:hover::after { | 450 | .hover-popup:hover::after { |
| 445 | visibility: visible; | 451 | visibility: visible; |
diff --git a/frontend/src/components/pages/summary_modview.css b/frontend/src/css/ModMenu.css index c6d3d8d..c6d3d8d 100644 --- a/frontend/src/components/pages/summary_modview.css +++ b/frontend/src/css/ModMenu.css | |||
diff --git a/frontend/src/components/pages/profile.css b/frontend/src/css/Profile.css index 4944ade..4944ade 100644 --- a/frontend/src/components/pages/profile.css +++ b/frontend/src/css/Profile.css | |||
diff --git a/frontend/src/components/sidebar.css b/frontend/src/css/Sidebar.css index 34ede80..34ede80 100644 --- a/frontend/src/components/sidebar.css +++ b/frontend/src/css/Sidebar.css | |||
diff --git a/frontend/src/images/Images.tsx b/frontend/src/images/Images.tsx new file mode 100644 index 0000000..d2f6dfb --- /dev/null +++ b/frontend/src/images/Images.tsx | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | import logo from "./png/logo.png" | ||
| 2 | import login from "./png/login.png" | ||
| 3 | import img1 from './png/1.png'; | ||
| 4 | import img2 from './png/2.png'; | ||
| 5 | import img3 from './png/3.png'; | ||
| 6 | import img4 from './png/4.png'; | ||
| 7 | import img5 from './png/5.png'; | ||
| 8 | import img6 from './png/6.png'; | ||
| 9 | import img7 from './png/7.png'; | ||
| 10 | import img8 from './png/8.png'; | ||
| 11 | import img9 from './png/9.png'; | ||
| 12 | import img10 from './png/10.png'; | ||
| 13 | import img11 from './png/11.png'; | ||
| 14 | import img12 from './png/12.png'; | ||
| 15 | import img13 from './png/13.png'; | ||
| 16 | import img14 from './png/14.png'; | ||
| 17 | import img15 from './png/15.png'; | ||
| 18 | import img16 from './png/16.png'; | ||
| 19 | import img17 from './png/17.png'; | ||
| 20 | import img18 from './png/18.png'; | ||
| 21 | import img19 from './png/19.png'; | ||
| 22 | |||
| 23 | export const LogoIcon = logo; | ||
| 24 | export const LoginIcon = login; | ||
| 25 | |||
| 26 | export const SearchIcon = img1; | ||
| 27 | export const HomeIcon = img2; | ||
| 28 | export const NewsIcon = img3; | ||
| 29 | export const PortalIcon = img4; | ||
| 30 | export const FlagIcon = img5; | ||
| 31 | export const ChatIcon = img6; | ||
| 32 | export const TableIcon = img7; | ||
| 33 | export const BookIcon = img8; | ||
| 34 | export const HelpIcon = img9; | ||
| 35 | export const UserIcon = img10; | ||
| 36 | export const ExitIcon = img11; | ||
| 37 | export const DownloadIcon = img12; | ||
| 38 | export const ThreedotIcon = img13; | ||
| 39 | export const StatisticsIcon = img14; | ||
| 40 | export const TwitchIcon = img15; | ||
| 41 | export const YouTubeIcon = img16; | ||
| 42 | export const SteamIcon = img17; | ||
| 43 | export const HistoryIcon = img18; | ||
| 44 | export const SortIcon = img19; \ No newline at end of file | ||
diff --git a/frontend/src/imgs/1.png b/frontend/src/images/png/1.png index ea59d2f..ea59d2f 100644 --- a/frontend/src/imgs/1.png +++ b/frontend/src/images/png/1.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/10.png b/frontend/src/images/png/10.png index d4b0863..d4b0863 100644 --- a/frontend/src/imgs/10.png +++ b/frontend/src/images/png/10.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/11.png b/frontend/src/images/png/11.png index b493059..b493059 100644 --- a/frontend/src/imgs/11.png +++ b/frontend/src/images/png/11.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/12.png b/frontend/src/images/png/12.png index abb7717..abb7717 100644 --- a/frontend/src/imgs/12.png +++ b/frontend/src/images/png/12.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/13.png b/frontend/src/images/png/13.png index 28a67c5..28a67c5 100644 --- a/frontend/src/imgs/13.png +++ b/frontend/src/images/png/13.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/14.png b/frontend/src/images/png/14.png index 7be6359..7be6359 100644 --- a/frontend/src/imgs/14.png +++ b/frontend/src/images/png/14.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/15.png b/frontend/src/images/png/15.png index e5ae8aa..e5ae8aa 100644 --- a/frontend/src/imgs/15.png +++ b/frontend/src/images/png/15.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/16.png b/frontend/src/images/png/16.png index bf3ae0c..bf3ae0c 100644 --- a/frontend/src/imgs/16.png +++ b/frontend/src/images/png/16.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/17.png b/frontend/src/images/png/17.png index 85e39f0..85e39f0 100644 --- a/frontend/src/imgs/17.png +++ b/frontend/src/images/png/17.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/18.png b/frontend/src/images/png/18.png index 048cda9..048cda9 100644 --- a/frontend/src/imgs/18.png +++ b/frontend/src/images/png/18.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/19.png b/frontend/src/images/png/19.png index 0d97d16..0d97d16 100644 --- a/frontend/src/imgs/19.png +++ b/frontend/src/images/png/19.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/2.png b/frontend/src/images/png/2.png index b8d108e..b8d108e 100644 --- a/frontend/src/imgs/2.png +++ b/frontend/src/images/png/2.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/3.png b/frontend/src/images/png/3.png index cfda6a4..cfda6a4 100644 --- a/frontend/src/imgs/3.png +++ b/frontend/src/images/png/3.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/4.png b/frontend/src/images/png/4.png index bbc01c4..bbc01c4 100644 --- a/frontend/src/imgs/4.png +++ b/frontend/src/images/png/4.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/5.png b/frontend/src/images/png/5.png index b63d2c3..b63d2c3 100644 --- a/frontend/src/imgs/5.png +++ b/frontend/src/images/png/5.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/6.png b/frontend/src/images/png/6.png index 6ced542..6ced542 100644 --- a/frontend/src/imgs/6.png +++ b/frontend/src/images/png/6.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/7.png b/frontend/src/images/png/7.png index c20bcf4..c20bcf4 100644 --- a/frontend/src/imgs/7.png +++ b/frontend/src/images/png/7.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/8.png b/frontend/src/images/png/8.png index d640522..d640522 100644 --- a/frontend/src/imgs/8.png +++ b/frontend/src/images/png/8.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/9.png b/frontend/src/images/png/9.png index 3cd602a..3cd602a 100644 --- a/frontend/src/imgs/9.png +++ b/frontend/src/images/png/9.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/login.png b/frontend/src/images/png/login.png index 6456c21..6456c21 100644 --- a/frontend/src/imgs/login.png +++ b/frontend/src/images/png/login.png | |||
| Binary files differ | |||
diff --git a/frontend/src/imgs/logo.png b/frontend/src/images/png/logo.png index 774d55a..774d55a 100644 --- a/frontend/src/imgs/logo.png +++ b/frontend/src/images/png/logo.png | |||
| Binary files differ | |||
diff --git a/frontend/src/index.js b/frontend/src/index.js deleted file mode 100644 index f648298..0000000 --- a/frontend/src/index.js +++ /dev/null | |||
| @@ -1,8 +0,0 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import ReactDOM from 'react-dom/client'; | ||
| 3 | import App from './App'; | ||
| 4 | |||
| 5 | const root = ReactDOM.createRoot(document.getElementById('root')); | ||
| 6 | root.render( | ||
| 7 | <App/> | ||
| 8 | ); | ||
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..eec2ff4 --- /dev/null +++ b/frontend/src/index.tsx | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import ReactDOM from 'react-dom/client'; | ||
| 3 | import { BrowserRouter } from "react-router-dom"; | ||
| 4 | |||
| 5 | import App from './App'; | ||
| 6 | |||
| 7 | const root = ReactDOM.createRoot( | ||
| 8 | document.getElementById('root') as HTMLElement | ||
| 9 | ); | ||
| 10 | |||
| 11 | root.render( | ||
| 12 | <React.StrictMode> | ||
| 13 | <BrowserRouter> | ||
| 14 | <App /> | ||
| 15 | </BrowserRouter> | ||
| 16 | </React.StrictMode> | ||
| 17 | ); | ||
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx new file mode 100644 index 0000000..e4b33e5 --- /dev/null +++ b/frontend/src/pages/Games.tsx | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | |||
| 3 | import GameEntry from '../components/GameEntry'; | ||
| 4 | import { Game } from '../types/Game'; | ||
| 5 | import { API } from '../api/Api'; | ||
| 6 | import "../css/Maps.css" | ||
| 7 | |||
| 8 | const Games: React.FC = () => { | ||
| 9 | const [games, setGames] = React.useState<Game[]>([]); | ||
| 10 | |||
| 11 | const _fetch_games = async () => { | ||
| 12 | const games = await API.get_games(); | ||
| 13 | setGames(games); | ||
| 14 | }; | ||
| 15 | |||
| 16 | const _page_load = () => { | ||
| 17 | const loaders = document.querySelectorAll(".loader"); | ||
| 18 | loaders.forEach((loader) => { | ||
| 19 | (loader as HTMLElement).style.display = "none"; | ||
| 20 | }); | ||
| 21 | } | ||
| 22 | |||
| 23 | React.useEffect(() => { | ||
| 24 | document.querySelectorAll(".games-page-item-body").forEach((game, index) => { | ||
| 25 | game.innerHTML = ""; | ||
| 26 | }); | ||
| 27 | |||
| 28 | _fetch_games(); | ||
| 29 | _page_load(); | ||
| 30 | }, []); | ||
| 31 | |||
| 32 | return ( | ||
| 33 | <div className='games-page'> | ||
| 34 | <section className='games-page-header'> | ||
| 35 | <span><b>Games list</b></span> | ||
| 36 | </section> | ||
| 37 | |||
| 38 | <section> | ||
| 39 | <div className='games-page-content'> | ||
| 40 | <div className='games-page-item-content'> | ||
| 41 | {games.map((game, index) => ( | ||
| 42 | <GameEntry game={game} key={index} /> | ||
| 43 | ))} | ||
| 44 | </div> | ||
| 45 | </div> | ||
| 46 | </section> | ||
| 47 | </div> | ||
| 48 | ); | ||
| 49 | }; | ||
| 50 | |||
| 51 | export default Games; | ||
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx new file mode 100644 index 0000000..707d865 --- /dev/null +++ b/frontend/src/pages/Maps.tsx | |||
| @@ -0,0 +1,91 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { Link, useLocation } from 'react-router-dom'; | ||
| 3 | |||
| 4 | import { PortalIcon, FlagIcon, ChatIcon } from '../images/Images'; | ||
| 5 | import Summary from '../components/Summary'; | ||
| 6 | import Leaderboards from '../components/Leaderboards'; | ||
| 7 | import Discussions from '../components/Discussions'; | ||
| 8 | import ModMenu from '../components/ModMenu'; | ||
| 9 | import { MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map'; | ||
| 10 | import { API } from '../api/Api'; | ||
| 11 | import "../css/Maps.css"; | ||
| 12 | |||
| 13 | interface MapProps { | ||
| 14 | isModerator: boolean; | ||
| 15 | }; | ||
| 16 | |||
| 17 | const Maps: React.FC<MapProps> = ({ isModerator }) => { | ||
| 18 | |||
| 19 | const [selectedRun, setSelectedRun] = React.useState<number>(0); | ||
| 20 | |||
| 21 | const [mapSummaryData, setMapSummaryData] = React.useState<MapSummary | undefined>(undefined); | ||
| 22 | const [mapLeaderboardData, setMapLeaderboardData] = React.useState<MapLeaderboard | undefined>(undefined); | ||
| 23 | const [mapDiscussionsData, setMapDiscussionsData] = React.useState<MapDiscussions | undefined>(undefined) | ||
| 24 | |||
| 25 | |||
| 26 | const [navState, setNavState] = React.useState<number>(0); | ||
| 27 | |||
| 28 | const location = useLocation(); | ||
| 29 | |||
| 30 | const mapID = location.pathname.split("/")[2]; | ||
| 31 | |||
| 32 | const _fetch_map_summary = async () => { | ||
| 33 | const mapSummary = await API.get_map_summary(mapID); | ||
| 34 | setMapSummaryData(mapSummary); | ||
| 35 | }; | ||
| 36 | |||
| 37 | const _fetch_map_leaderboards = async () => { | ||
| 38 | const mapLeaderboards = await API.get_map_leaderboard(mapID); | ||
| 39 | setMapLeaderboardData(mapLeaderboards); | ||
| 40 | }; | ||
| 41 | |||
| 42 | const _fetch_map_discussions = async () => { | ||
| 43 | const mapDiscussions = await API.get_map_discussions(mapID); | ||
| 44 | setMapDiscussionsData(mapDiscussions); | ||
| 45 | }; | ||
| 46 | |||
| 47 | React.useEffect(() => { | ||
| 48 | _fetch_map_summary(); | ||
| 49 | _fetch_map_leaderboards(); | ||
| 50 | _fetch_map_discussions(); | ||
| 51 | }, []); | ||
| 52 | |||
| 53 | if (!mapSummaryData) { | ||
| 54 | return ( | ||
| 55 | <></> | ||
| 56 | ); | ||
| 57 | } | ||
| 58 | |||
| 59 | return ( | ||
| 60 | <> | ||
| 61 | {isModerator && <ModMenu data={mapSummaryData} selectedRun={selectedRun} mapID={mapID} />} | ||
| 62 | |||
| 63 | <div id='background-image'> | ||
| 64 | <img src={mapSummaryData.map.image} alt="" /> | ||
| 65 | </div> | ||
| 66 | <main> | ||
| 67 | <section id='section1' className='summary1'> | ||
| 68 | <div> | ||
| 69 | <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 0px 0px 20px" }}><i className='triangle'></i><span>Games list</span></button></Link> | ||
| 70 | <Link to={`/games/${!mapSummaryData.map.is_coop ? "1" : "2"}?chapter=${mapSummaryData.map.chapter_name}`}><button className='nav-button' style={{ borderRadius: "0px 20px 20px 0px", marginLeft: "2px" }}><i className='triangle'></i><span>{mapSummaryData.map.chapter_name}</span></button></Link> | ||
| 71 | <br /><span><b>{mapSummaryData.map.map_name}</b></span> | ||
| 72 | </div> | ||
| 73 | |||
| 74 | |||
| 75 | </section> | ||
| 76 | |||
| 77 | <section id='section2' className='summary1'> | ||
| 78 | <button className='nav-button' onClick={() => setNavState(0)}><img src={PortalIcon} alt="" /><span>Summary</span></button> | ||
| 79 | <button className='nav-button' onClick={() => setNavState(1)}><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> | ||
| 80 | <button className='nav-button' onClick={() => setNavState(2)}><img src={ChatIcon} alt="" /><span>Discussions</span></button> | ||
| 81 | </section> | ||
| 82 | |||
| 83 | {navState === 0 && <Summary selectedRun={selectedRun} setSelectedRun={setSelectedRun} data={mapSummaryData} />} | ||
| 84 | {navState === 1 && <Leaderboards data={mapLeaderboardData} />} | ||
| 85 | {navState === 2 && <Discussions data={mapDiscussionsData} isModerator={isModerator} mapID={mapID} onRefresh={() => _fetch_map_discussions()} />} | ||
| 86 | </main> | ||
| 87 | </> | ||
| 88 | ); | ||
| 89 | }; | ||
| 90 | |||
| 91 | export default Maps; | ||
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx new file mode 100644 index 0000000..8654a03 --- /dev/null +++ b/frontend/src/pages/Profile.tsx | |||
| @@ -0,0 +1,326 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { useLocation, useNavigate } from 'react-router-dom'; | ||
| 3 | |||
| 4 | import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '../images/Images'; | ||
| 5 | import { UserProfile } from '../types/Profile'; | ||
| 6 | import { Game, GameChapters } from '../types/Game'; | ||
| 7 | import { Map } from '../types/Map'; | ||
| 8 | import "../css/Profile.css"; | ||
| 9 | |||
| 10 | interface ProfileProps { | ||
| 11 | profile: UserProfile; | ||
| 12 | } | ||
| 13 | |||
| 14 | const Profile: React.FC<ProfileProps> = ({ profile }) => { | ||
| 15 | |||
| 16 | |||
| 17 | const location = useLocation(); | ||
| 18 | const navigate = useNavigate(); | ||
| 19 | |||
| 20 | React.useEffect(() => { | ||
| 21 | if (!profile) { | ||
| 22 | navigate("/"); | ||
| 23 | }; | ||
| 24 | }, [profile]); | ||
| 25 | |||
| 26 | const [navState, setNavState] = React.useState(0); | ||
| 27 | const [pageNumber, setPageNumber] = React.useState(1); | ||
| 28 | const [pageMax, setPageMax] = React.useState(0); | ||
| 29 | |||
| 30 | const [game, setGame] = React.useState("0") | ||
| 31 | const [gameData, setGameData] = React.useState<Game[]>([]); | ||
| 32 | const [chapter, setChapter] = React.useState("0") | ||
| 33 | const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); | ||
| 34 | const [maps, setMaps] = React.useState<Map[]>([]); | ||
| 35 | |||
| 36 | function NavClick() { | ||
| 37 | if (profile) { | ||
| 38 | const btn = document.querySelectorAll("#section2 button"); | ||
| 39 | btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); | ||
| 40 | (btn[navState] as HTMLElement).style.backgroundColor = "#202232"; | ||
| 41 | |||
| 42 | document.querySelectorAll("section").forEach((e, i) => i >= 2 ? e.style.display = "none" : "") | ||
| 43 | if (navState === 0) { document.querySelectorAll(".profile1").forEach((e) => { (e as HTMLElement).style.display = "block" }); } | ||
| 44 | if (navState === 1) { document.querySelectorAll(".profile2").forEach((e) => { (e as HTMLElement).style.display = "block" }); } | ||
| 45 | } | ||
| 46 | } | ||
| 47 | |||
| 48 | function UpdateProfile() { | ||
| 49 | fetch(`https://lp.ardapektezol.com/api/v1/profile`, { | ||
| 50 | method: 'POST', | ||
| 51 | headers: { Authorization: "" } | ||
| 52 | }).then(r => r.json()) | ||
| 53 | .then(d => d.success ? window.alert("profile updated") : window.alert(`Error: ${d.message}`)) | ||
| 54 | } | ||
| 55 | |||
| 56 | function TicksToTime(ticks: number) { | ||
| 57 | |||
| 58 | let seconds = Math.floor(ticks / 60) | ||
| 59 | let minutes = Math.floor(seconds / 60) | ||
| 60 | let hours = Math.floor(minutes / 60) | ||
| 61 | |||
| 62 | let milliseconds = Math.floor((ticks % 60) * 1000 / 60) | ||
| 63 | seconds = seconds % 60; | ||
| 64 | minutes = minutes % 60; | ||
| 65 | |||
| 66 | 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')} (${ticks})`; | ||
| 67 | } | ||
| 68 | |||
| 69 | React.useEffect(() => { | ||
| 70 | fetch("https://lp.ardapektezol.com/api/v1/games") | ||
| 71 | .then(r => r.json()) | ||
| 72 | .then(d => { | ||
| 73 | setGameData(d.data) | ||
| 74 | setGame("0") | ||
| 75 | }) | ||
| 76 | |||
| 77 | }, [location]); | ||
| 78 | |||
| 79 | React.useEffect(() => { | ||
| 80 | if (game && game !== "0") { | ||
| 81 | fetch(`https://lp.ardapektezol.com/api/v1/games/${game}`) | ||
| 82 | .then(r => r.json()) | ||
| 83 | .then(d => { | ||
| 84 | setChapterData(d.data) | ||
| 85 | setChapter("0"); | ||
| 86 | // (document.querySelector('#select-chapter') as HTMLInputElement).value = "0" | ||
| 87 | }) | ||
| 88 | |||
| 89 | } else if (game && game === "0") { | ||
| 90 | setPageMax(Math.ceil(profile.records.length / 20)) | ||
| 91 | setPageNumber(1) | ||
| 92 | } | ||
| 93 | |||
| 94 | }, [game, location]); | ||
| 95 | |||
| 96 | React.useEffect(() => { | ||
| 97 | if (game !== "0") { | ||
| 98 | if (chapter === "0") { | ||
| 99 | fetch(`https://lp.ardapektezol.com/api/v1/games/${game}/maps`) | ||
| 100 | .then(r => r.json()) | ||
| 101 | .then(d => { | ||
| 102 | setMaps(d.data.maps); | ||
| 103 | setPageMax(Math.ceil(d.data.maps.length / 20)) | ||
| 104 | setPageNumber(1) | ||
| 105 | }) | ||
| 106 | } else { | ||
| 107 | fetch(`https://lp.ardapektezol.com/api/v1/chapters/${chapter}`) | ||
| 108 | .then(r => r.json()) | ||
| 109 | .then(d => { | ||
| 110 | setMaps(d.data.maps); | ||
| 111 | setPageMax(Math.ceil(d.data.maps.length / 20)) | ||
| 112 | setPageNumber(1) | ||
| 113 | }) | ||
| 114 | |||
| 115 | } | ||
| 116 | } | ||
| 117 | }, [game, chapter, chapterData]) | ||
| 118 | |||
| 119 | return ( | ||
| 120 | <main> | ||
| 121 | <section id='section1' className='profile'> | ||
| 122 | |||
| 123 | {profile.profile | ||
| 124 | ? ( | ||
| 125 | <div id='profile-image' onClick={() => UpdateProfile()}> | ||
| 126 | <img src={profile.avatar_link} alt="profile-image"></img> | ||
| 127 | <span>Refresh</span> | ||
| 128 | </div> | ||
| 129 | ) : ( | ||
| 130 | <div> | ||
| 131 | <img src={profile.avatar_link} alt="profile-image"></img> | ||
| 132 | </div> | ||
| 133 | )} | ||
| 134 | |||
| 135 | <div id='profile-top'> | ||
| 136 | <div> | ||
| 137 | <div>{profile.user_name}</div> | ||
| 138 | <div> | ||
| 139 | {profile.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`} alt={profile.country_code} />} | ||
| 140 | </div> | ||
| 141 | <div> | ||
| 142 | {profile.titles.map(e => ( | ||
| 143 | <span className="titles" style={{ backgroundColor: `#${e.color}` }}> | ||
| 144 | {e.name} | ||
| 145 | </span> | ||
| 146 | ))} | ||
| 147 | </div> | ||
| 148 | </div> | ||
| 149 | <div> | ||
| 150 | {profile.links.steam === "-" ? "" : <a href={profile.links.steam}><img src={SteamIcon} alt="Steam" /></a>} | ||
| 151 | {profile.links.twitch === "-" ? "" : <a href={profile.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} | ||
| 152 | {profile.links.youtube === "-" ? "" : <a href={profile.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} | ||
| 153 | {profile.links.p2sr === "-" ? "" : <a href={profile.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} | ||
| 154 | </div> | ||
| 155 | |||
| 156 | </div> | ||
| 157 | <div id='profile-bottom'> | ||
| 158 | <div> | ||
| 159 | <span>Overall</span> | ||
| 160 | <span>{profile.rankings.overall.rank === 0 ? "N/A " : "#" + profile.rankings.overall.rank + " "} | ||
| 161 | <span>({profile.rankings.overall.completion_count}/{profile.rankings.overall.completion_total})</span> | ||
| 162 | </span> | ||
| 163 | </div> | ||
| 164 | <div> | ||
| 165 | <span>Singleplayer</span> | ||
| 166 | <span>{profile.rankings.singleplayer.rank === 0 ? "N/A " : "#" + profile.rankings.singleplayer.rank + " "} | ||
| 167 | <span>({profile.rankings.singleplayer.completion_count}/{profile.rankings.singleplayer.completion_total})</span> | ||
| 168 | </span> | ||
| 169 | </div> | ||
| 170 | <div> | ||
| 171 | <span>Cooperative</span> | ||
| 172 | <span>{profile.rankings.cooperative.rank === 0 ? "N/A " : "#" + profile.rankings.cooperative.rank + " "} | ||
| 173 | <span>({profile.rankings.cooperative.completion_count}/{profile.rankings.cooperative.completion_total})</span> | ||
| 174 | </span> | ||
| 175 | </div> | ||
| 176 | </div> | ||
| 177 | </section> | ||
| 178 | |||
| 179 | |||
| 180 | <section id='section2' className='profile'> | ||
| 181 | <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" /> Player Records</button> | ||
| 182 | <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" /> Statistics</button> | ||
| 183 | </section> | ||
| 184 | |||
| 185 | |||
| 186 | |||
| 187 | |||
| 188 | |||
| 189 | <section id='section3' className='profile1'> | ||
| 190 | <div id='profileboard-nav'> | ||
| 191 | {gameData === null ? <select>error</select> : | ||
| 192 | |||
| 193 | <select id='select-game' | ||
| 194 | onChange={() => setGame((document.querySelector('#select-game') as HTMLInputElement).value)}> | ||
| 195 | <option value={0} key={0}>All Scores</option> | ||
| 196 | {gameData.map((e, i) => ( | ||
| 197 | <option value={e.id} key={i + 1}>{e.name}</option> | ||
| 198 | ))}</select> | ||
| 199 | } | ||
| 200 | |||
| 201 | {game === "0" ? | ||
| 202 | <select disabled> | ||
| 203 | <option>All Scores</option> | ||
| 204 | </select> | ||
| 205 | : chapterData === null ? <select></select> : | ||
| 206 | |||
| 207 | <select id='select-chapter' | ||
| 208 | onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> | ||
| 209 | <option value="0" key="0">All</option> | ||
| 210 | {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( | ||
| 211 | <option value={e.id} key={i + 1}>{e.name}</option> | ||
| 212 | ))}</select> | ||
| 213 | } | ||
| 214 | </div> | ||
| 215 | <div id='profileboard-top'> | ||
| 216 | <span><span>Map Name</span><img src={SortIcon} alt="" /></span> | ||
| 217 | <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> | ||
| 218 | <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> | ||
| 219 | <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> | ||
| 220 | <span> </span> | ||
| 221 | <span><span>Rank</span><img src={SortIcon} alt="" /></span> | ||
| 222 | <span><span>Date</span><img src={SortIcon} alt="" /></span> | ||
| 223 | <div id='page-number'> | ||
| 224 | <div> | ||
| 225 | <button onClick={() => pageNumber === 1 ? null : setPageNumber(prevPageNumber => prevPageNumber - 1)} | ||
| 226 | ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> | ||
| 227 | <span>{pageNumber}/{pageMax}</span> | ||
| 228 | <button onClick={() => pageNumber === pageMax ? null : setPageNumber(prevPageNumber => prevPageNumber + 1)} | ||
| 229 | ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> | ||
| 230 | </div> | ||
| 231 | </div> | ||
| 232 | </div> | ||
| 233 | <hr /> | ||
| 234 | <div id='profileboard-records'> | ||
| 235 | |||
| 236 | {game === "0" | ||
| 237 | ? ( | ||
| 238 | |||
| 239 | profile.records.sort((a, b) => a.map_id - b.map_id) | ||
| 240 | .map((r, index) => ( | ||
| 241 | |||
| 242 | Math.ceil((index + 1) / 20) === pageNumber ? ( | ||
| 243 | <button className="profileboard-record" key={index}> | ||
| 244 | {r.scores.map((e, i) => (<> | ||
| 245 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | ||
| 246 | |||
| 247 | <span>{r.map_name}</span> | ||
| 248 | |||
| 249 | <span style={{ display: "grid" }}>{e.score_count}</span> | ||
| 250 | |||
| 251 | <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count}</span> | ||
| 252 | <span style={{ display: "grid" }}>{TicksToTime(e.score_time)}</span> | ||
| 253 | <span> </span> | ||
| 254 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} | ||
| 255 | <span>{e.date.split("T")[0]}</span> | ||
| 256 | <span style={{ flexDirection: "row-reverse" }}> | ||
| 257 | |||
| 258 | <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | ||
| 259 | <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | ||
| 260 | {i === 0 && r.scores.length > 1 ? <button onClick={() => { | ||
| 261 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | ||
| 262 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | ||
| 263 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : | ||
| 264 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | ||
| 265 | } | ||
| 266 | }><img src={HistoryIcon} alt="history" /></button> : ""} | ||
| 267 | |||
| 268 | </span> | ||
| 269 | </>))} | ||
| 270 | |||
| 271 | </button> | ||
| 272 | ) : "" | ||
| 273 | ))) : maps ? | ||
| 274 | |||
| 275 | maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) | ||
| 276 | .map((r, index) => { | ||
| 277 | if (Math.ceil((index + 1) / 20) === pageNumber) { | ||
| 278 | let record = profile.records.find((e) => e.map_id === r.id); | ||
| 279 | return record === undefined ? ( | ||
| 280 | <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> | ||
| 281 | <span>{r.name}</span> | ||
| 282 | <span style={{ display: "grid" }}>N/A</span> | ||
| 283 | <span style={{ display: "grid" }}>N/A</span> | ||
| 284 | <span>N/A</span> | ||
| 285 | <span> </span> | ||
| 286 | <span>N/A</span> | ||
| 287 | <span>N/A</span> | ||
| 288 | <span style={{ flexDirection: "row-reverse" }}></span> | ||
| 289 | </button> | ||
| 290 | ) : ( | ||
| 291 | <button className="profileboard-record" key={index}> | ||
| 292 | {record.scores.map((e, i) => (<> | ||
| 293 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | ||
| 294 | <span>{r.name}</span> | ||
| 295 | <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> | ||
| 296 | <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count}</span> | ||
| 297 | <span style={{ display: "grid" }}>{TicksToTime(record!.scores[i].score_time)}</span> | ||
| 298 | <span> </span> | ||
| 299 | {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} | ||
| 300 | <span>{record!.scores[i].date.split("T")[0]}</span> | ||
| 301 | <span style={{ flexDirection: "row-reverse" }}> | ||
| 302 | |||
| 303 | <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | ||
| 304 | <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | ||
| 305 | {i === 0 && record!.scores.length > 1 ? <button onClick={() => { | ||
| 306 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | ||
| 307 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | ||
| 308 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : | ||
| 309 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | ||
| 310 | } | ||
| 311 | }><img src={HistoryIcon} alt="history" /></button> : ""} | ||
| 312 | |||
| 313 | </span> | ||
| 314 | </>))} | ||
| 315 | </button> | ||
| 316 | |||
| 317 | ) | ||
| 318 | } else { return null } | ||
| 319 | }) : (<>{console.warn(maps)}</>)} | ||
| 320 | </div> | ||
| 321 | </section> | ||
| 322 | </main> | ||
| 323 | ); | ||
| 324 | }; | ||
| 325 | |||
| 326 | export default Profile; | ||
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx new file mode 100644 index 0000000..1f6d8d0 --- /dev/null +++ b/frontend/src/pages/User.tsx | |||
| @@ -0,0 +1,320 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { useLocation } from 'react-router-dom'; | ||
| 3 | |||
| 4 | import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '../images/Images'; | ||
| 5 | import { UserProfile } from '../types/Profile'; | ||
| 6 | import { Game, GameChapters } from '../types/Game'; | ||
| 7 | import { Map } from '../types/Map'; | ||
| 8 | import { API } from '../api/Api'; | ||
| 9 | import { ticks_to_time } from '../utils/Time'; | ||
| 10 | import "../css/Profile.css"; | ||
| 11 | |||
| 12 | const User: React.FC = () => { | ||
| 13 | const location = useLocation(); | ||
| 14 | |||
| 15 | const [user, setUser] = React.useState<UserProfile | undefined>(undefined); | ||
| 16 | |||
| 17 | const [navState, setNavState] = React.useState(0); | ||
| 18 | const [pageNumber, setPageNumber] = React.useState(1); | ||
| 19 | const [pageMax, setPageMax] = React.useState(0); | ||
| 20 | |||
| 21 | const [game, setGame] = React.useState("0") | ||
| 22 | const [gameData, setGameData] = React.useState<Game[]>([]); | ||
| 23 | const [chapter, setChapter] = React.useState("0") | ||
| 24 | const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); | ||
| 25 | const [maps, setMaps] = React.useState<Map[]>([]); | ||
| 26 | |||
| 27 | function NavClick() { | ||
| 28 | if (user) { | ||
| 29 | const btn = document.querySelectorAll("#section2 button"); | ||
| 30 | btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); | ||
| 31 | (btn[navState] as HTMLElement).style.backgroundColor = "#202232"; | ||
| 32 | |||
| 33 | document.querySelectorAll("section").forEach((e, i) => i >= 2 ? e.style.display = "none" : "") | ||
| 34 | if (navState === 0) { document.querySelectorAll(".profile1").forEach((e) => { (e as HTMLElement).style.display = "block" }); } | ||
| 35 | if (navState === 1) { document.querySelectorAll(".profile2").forEach((e) => { (e as HTMLElement).style.display = "block" }); } | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | function UpdateProfile() { | ||
| 40 | fetch(`https://lp.ardapektezol.com/api/v1/profile`, { | ||
| 41 | method: 'POST', | ||
| 42 | headers: { Authorization: "" } | ||
| 43 | }).then(r => r.json()) | ||
| 44 | .then(d => d.success ? window.alert("profile updated") : window.alert(`Error: ${d.message}`)) | ||
| 45 | } | ||
| 46 | |||
| 47 | const _fetch_user = async () => { | ||
| 48 | const userData = await API.get_user(location.pathname.split("/")[2]); | ||
| 49 | setUser(userData); | ||
| 50 | }; | ||
| 51 | |||
| 52 | React.useEffect(() => { | ||
| 53 | fetch("https://lp.ardapektezol.com/api/v1/games") | ||
| 54 | .then(r => r.json()) | ||
| 55 | .then(d => { | ||
| 56 | setGameData(d.data) | ||
| 57 | setGame("0") | ||
| 58 | }) | ||
| 59 | |||
| 60 | }, [location]); | ||
| 61 | |||
| 62 | React.useEffect(() => { | ||
| 63 | if (user) { | ||
| 64 | if (game && game !== "0") { | ||
| 65 | fetch(`https://lp.ardapektezol.com/api/v1/games/${game}`) | ||
| 66 | .then(r => r.json()) | ||
| 67 | .then(d => { | ||
| 68 | setChapterData(d.data) | ||
| 69 | setChapter("0"); | ||
| 70 | // (document.querySelector('#select-chapter') as HTMLInputElement).value = "0" | ||
| 71 | }) | ||
| 72 | |||
| 73 | } else if (game && game === "0") { | ||
| 74 | setPageMax(Math.ceil(user.records.length / 20)) | ||
| 75 | setPageNumber(1) | ||
| 76 | } | ||
| 77 | } | ||
| 78 | }, [user, game, location]); | ||
| 79 | |||
| 80 | React.useEffect(() => { | ||
| 81 | _fetch_user(); | ||
| 82 | }, []); | ||
| 83 | |||
| 84 | React.useEffect(() => { | ||
| 85 | if (game !== "0") { | ||
| 86 | if (chapter === "0") { | ||
| 87 | fetch(`https://lp.ardapektezol.com/api/v1/games/${game}/maps`) | ||
| 88 | .then(r => r.json()) | ||
| 89 | .then(d => { | ||
| 90 | setMaps(d.data.maps); | ||
| 91 | setPageMax(Math.ceil(d.data.maps.length / 20)) | ||
| 92 | setPageNumber(1) | ||
| 93 | }) | ||
| 94 | } else { | ||
| 95 | fetch(`https://lp.ardapektezol.com/api/v1/chapters/${chapter}`) | ||
| 96 | .then(r => r.json()) | ||
| 97 | .then(d => { | ||
| 98 | setMaps(d.data.maps); | ||
| 99 | setPageMax(Math.ceil(d.data.maps.length / 20)) | ||
| 100 | setPageNumber(1) | ||
| 101 | }) | ||
| 102 | |||
| 103 | } | ||
| 104 | } | ||
| 105 | }, [game, chapter, chapterData]) | ||
| 106 | |||
| 107 | if (!user) { | ||
| 108 | return ( | ||
| 109 | <></> | ||
| 110 | ); | ||
| 111 | }; | ||
| 112 | |||
| 113 | return ( | ||
| 114 | <main> | ||
| 115 | <section id='section1' className='profile'> | ||
| 116 | |||
| 117 | {user.profile | ||
| 118 | ? ( | ||
| 119 | <div id='profile-image' onClick={() => UpdateProfile()}> | ||
| 120 | <img src={user.avatar_link} alt="profile-image"></img> | ||
| 121 | <span>Refresh</span> | ||
| 122 | </div> | ||
| 123 | ) : ( | ||
| 124 | <div> | ||
| 125 | <img src={user.avatar_link} alt="profile-image"></img> | ||
| 126 | </div> | ||
| 127 | )} | ||
| 128 | |||
| 129 | <div id='profile-top'> | ||
| 130 | <div> | ||
| 131 | <div>{user.user_name}</div> | ||
| 132 | <div> | ||
| 133 | {user.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`} alt={user.country_code} />} | ||
| 134 | </div> | ||
| 135 | <div> | ||
| 136 | {user.titles.map(e => ( | ||
| 137 | <span className="titles" style={{ backgroundColor: `#${e.color}` }}> | ||
| 138 | {e.name} | ||
| 139 | </span> | ||
| 140 | ))} | ||
| 141 | </div> | ||
| 142 | </div> | ||
| 143 | <div> | ||
| 144 | {user.links.steam === "-" ? "" : <a href={user.links.steam}><img src={SteamIcon} alt="Steam" /></a>} | ||
| 145 | {user.links.twitch === "-" ? "" : <a href={user.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} | ||
| 146 | {user.links.youtube === "-" ? "" : <a href={user.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} | ||
| 147 | {user.links.p2sr === "-" ? "" : <a href={user.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} | ||
| 148 | </div> | ||
| 149 | |||
| 150 | </div> | ||
| 151 | <div id='profile-bottom'> | ||
| 152 | <div> | ||
| 153 | <span>Overall</span> | ||
| 154 | <span>{user.rankings.overall.rank === 0 ? "N/A " : "#" + user.rankings.overall.rank + " "} | ||
| 155 | <span>({user.rankings.overall.completion_count}/{user.rankings.overall.completion_total})</span> | ||
| 156 | </span> | ||
| 157 | </div> | ||
| 158 | <div> | ||
| 159 | <span>Singleplayer</span> | ||
| 160 | <span>{user.rankings.singleplayer.rank === 0 ? "N/A " : "#" + user.rankings.singleplayer.rank + " "} | ||
| 161 | <span>({user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total})</span> | ||
| 162 | </span> | ||
| 163 | </div> | ||
| 164 | <div> | ||
| 165 | <span>Cooperative</span> | ||
| 166 | <span>{user.rankings.cooperative.rank === 0 ? "N/A " : "#" + user.rankings.cooperative.rank + " "} | ||
| 167 | <span>({user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total})</span> | ||
| 168 | </span> | ||
| 169 | </div> | ||
| 170 | </div> | ||
| 171 | </section> | ||
| 172 | |||
| 173 | |||
| 174 | <section id='section2' className='profile'> | ||
| 175 | <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" /> Player Records</button> | ||
| 176 | <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" /> Statistics</button> | ||
| 177 | </section> | ||
| 178 | |||
| 179 | |||
| 180 | |||
| 181 | |||
| 182 | |||
| 183 | <section id='section3' className='profile1'> | ||
| 184 | <div id='profileboard-nav'> | ||
| 185 | {gameData === null ? <select>error</select> : | ||
| 186 | |||
| 187 | <select id='select-game' | ||
| 188 | onChange={() => setGame((document.querySelector('#select-game') as HTMLInputElement).value)}> | ||
| 189 | <option value={0} key={0}>All Scores</option> | ||
| 190 | {gameData.map((e, i) => ( | ||
| 191 | <option value={e.id} key={i + 1}>{e.name}</option> | ||
| 192 | ))}</select> | ||
| 193 | } | ||
| 194 | |||
| 195 | {game === "0" ? | ||
| 196 | <select disabled> | ||
| 197 | <option>All Scores</option> | ||
| 198 | </select> | ||
| 199 | : chapterData === null ? <select></select> : | ||
| 200 | |||
| 201 | <select id='select-chapter' | ||
| 202 | onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> | ||
| 203 | <option value="0" key="0">All</option> | ||
| 204 | {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( | ||
| 205 | <option value={e.id} key={i + 1}>{e.name}</option> | ||
| 206 | ))}</select> | ||
| 207 | } | ||
| 208 | </div> | ||
| 209 | <div id='profileboard-top'> | ||
| 210 | <span><span>Map Name</span><img src={SortIcon} alt="" /></span> | ||
| 211 | <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> | ||
| 212 | <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> | ||
| 213 | <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> | ||
| 214 | <span> </span> | ||
| 215 | <span><span>Rank</span><img src={SortIcon} alt="" /></span> | ||
| 216 | <span><span>Date</span><img src={SortIcon} alt="" /></span> | ||
| 217 | <div id='page-number'> | ||
| 218 | <div> | ||
| 219 | <button onClick={() => pageNumber === 1 ? null : setPageNumber(prevPageNumber => prevPageNumber - 1)} | ||
| 220 | ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> | ||
| 221 | <span>{pageNumber}/{pageMax}</span> | ||
| 222 | <button onClick={() => pageNumber === pageMax ? null : setPageNumber(prevPageNumber => prevPageNumber + 1)} | ||
| 223 | ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> | ||
| 224 | </div> | ||
| 225 | </div> | ||
| 226 | </div> | ||
| 227 | <hr /> | ||
| 228 | <div id='profileboard-records'> | ||
| 229 | |||
| 230 | {game === "0" | ||
| 231 | ? ( | ||
| 232 | |||
| 233 | user.records.sort((a, b) => a.map_id - b.map_id) | ||
| 234 | .map((r, index) => ( | ||
| 235 | |||
| 236 | Math.ceil((index + 1) / 20) === pageNumber ? ( | ||
| 237 | <button className="profileboard-record" key={index}> | ||
| 238 | {r.scores.map((e, i) => (<> | ||
| 239 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | ||
| 240 | |||
| 241 | <span>{r.map_name}</span> | ||
| 242 | |||
| 243 | <span style={{ display: "grid" }}>{e.score_count}</span> | ||
| 244 | |||
| 245 | <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count}</span> | ||
| 246 | <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> | ||
| 247 | <span> </span> | ||
| 248 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} | ||
| 249 | <span>{e.date.split("T")[0]}</span> | ||
| 250 | <span style={{ flexDirection: "row-reverse" }}> | ||
| 251 | |||
| 252 | <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | ||
| 253 | <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | ||
| 254 | {i === 0 && r.scores.length > 1 ? <button onClick={() => { | ||
| 255 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | ||
| 256 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | ||
| 257 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : | ||
| 258 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | ||
| 259 | } | ||
| 260 | }><img src={HistoryIcon} alt="history" /></button> : ""} | ||
| 261 | |||
| 262 | </span> | ||
| 263 | </>))} | ||
| 264 | |||
| 265 | </button> | ||
| 266 | ) : "" | ||
| 267 | ))) : maps ? | ||
| 268 | |||
| 269 | maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) | ||
| 270 | .map((r, index) => { | ||
| 271 | if (Math.ceil((index + 1) / 20) === pageNumber) { | ||
| 272 | let record = user.records.find((e) => e.map_id === r.id); | ||
| 273 | return record === undefined ? ( | ||
| 274 | <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> | ||
| 275 | <span>{r.name}</span> | ||
| 276 | <span style={{ display: "grid" }}>N/A</span> | ||
| 277 | <span style={{ display: "grid" }}>N/A</span> | ||
| 278 | <span>N/A</span> | ||
| 279 | <span> </span> | ||
| 280 | <span>N/A</span> | ||
| 281 | <span>N/A</span> | ||
| 282 | <span style={{ flexDirection: "row-reverse" }}></span> | ||
| 283 | </button> | ||
| 284 | ) : ( | ||
| 285 | <button className="profileboard-record" key={index}> | ||
| 286 | {record.scores.map((e, i) => (<> | ||
| 287 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | ||
| 288 | <span>{r.name}</span> | ||
| 289 | <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> | ||
| 290 | <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count}</span> | ||
| 291 | <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> | ||
| 292 | <span> </span> | ||
| 293 | {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} | ||
| 294 | <span>{record!.scores[i].date.split("T")[0]}</span> | ||
| 295 | <span style={{ flexDirection: "row-reverse" }}> | ||
| 296 | |||
| 297 | <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | ||
| 298 | <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | ||
| 299 | {i === 0 && record!.scores.length > 1 ? <button onClick={() => { | ||
| 300 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | ||
| 301 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | ||
| 302 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : | ||
| 303 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | ||
| 304 | } | ||
| 305 | }><img src={HistoryIcon} alt="history" /></button> : ""} | ||
| 306 | |||
| 307 | </span> | ||
| 308 | </>))} | ||
| 309 | </button> | ||
| 310 | |||
| 311 | ) | ||
| 312 | } else { return null } | ||
| 313 | }) : (<>{console.warn(maps)}</>)} | ||
| 314 | </div> | ||
| 315 | </section> | ||
| 316 | </main> | ||
| 317 | ); | ||
| 318 | }; | ||
| 319 | |||
| 320 | export default User; | ||
diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts new file mode 100644 index 0000000..8265915 --- /dev/null +++ b/frontend/src/react-app-env.d.ts | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | declare module "*.png"; | ||
| 2 | declare module "*.css"; | ||
diff --git a/frontend/src/types/Content.tsx b/frontend/src/types/Content.tsx new file mode 100644 index 0000000..e593505 --- /dev/null +++ b/frontend/src/types/Content.tsx | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | export interface ModMenuContent { | ||
| 2 | id: number; | ||
| 3 | name: string; | ||
| 4 | score: number; | ||
| 5 | date: string; | ||
| 6 | showcase: string; | ||
| 7 | description: string; | ||
| 8 | category_id: number; | ||
| 9 | }; | ||
| 10 | |||
| 11 | export interface MapDiscussionContent { | ||
| 12 | title: string; | ||
| 13 | content: string; | ||
| 14 | }; | ||
| 15 | |||
| 16 | export interface MapDiscussionCommentContent { | ||
| 17 | comment: string; | ||
| 18 | }; | ||
diff --git a/frontend/src/types/Game.tsx b/frontend/src/types/Game.tsx new file mode 100644 index 0000000..eb435f6 --- /dev/null +++ b/frontend/src/types/Game.tsx | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | import { Map } from './Map'; | ||
| 2 | |||
| 3 | |||
| 4 | export interface Game { | ||
| 5 | id: number; | ||
| 6 | name: string; | ||
| 7 | image: string; | ||
| 8 | is_coop: boolean; | ||
| 9 | category_portals: GameCategoryPortals[]; | ||
| 10 | }; | ||
| 11 | |||
| 12 | export interface GameChapters { | ||
| 13 | game: Game; | ||
| 14 | chapters: Chapter[]; | ||
| 15 | }; | ||
| 16 | |||
| 17 | export interface GameMaps { | ||
| 18 | game: Game; | ||
| 19 | maps: Map[]; | ||
| 20 | }; | ||
| 21 | |||
| 22 | export interface Category { | ||
| 23 | id: number; | ||
| 24 | name: string; | ||
| 25 | }; | ||
| 26 | |||
| 27 | interface Chapter { | ||
| 28 | id: number; | ||
| 29 | name: string; | ||
| 30 | image: string; | ||
| 31 | is_disabled: boolean; | ||
| 32 | }; | ||
| 33 | |||
| 34 | export interface GameCategoryPortals { | ||
| 35 | category: Category; | ||
| 36 | portal_count: number; | ||
| 37 | }; | ||
diff --git a/frontend/src/types/Map.tsx b/frontend/src/types/Map.tsx new file mode 100644 index 0000000..4a6b60e --- /dev/null +++ b/frontend/src/types/Map.tsx | |||
| @@ -0,0 +1,103 @@ | |||
| 1 | import { Category, GameCategoryPortals } from './Game'; | ||
| 2 | import { Pagination } from './Pagination'; | ||
| 3 | import { UserShort } from './Profile'; | ||
| 4 | |||
| 5 | export interface Map { | ||
| 6 | id: number; | ||
| 7 | name: string; | ||
| 8 | image: string; | ||
| 9 | is_disabled: boolean; | ||
| 10 | difficulty: number; | ||
| 11 | category_portals: GameCategoryPortals[]; | ||
| 12 | }; | ||
| 13 | |||
| 14 | export interface MapDiscussion { | ||
| 15 | discussion: MapDiscussionsDetail; | ||
| 16 | }; | ||
| 17 | |||
| 18 | export interface MapDiscussions { | ||
| 19 | discussions: MapDiscussionsDetail[]; | ||
| 20 | }; | ||
| 21 | |||
| 22 | export interface MapDiscussionsDetail { | ||
| 23 | id: number; | ||
| 24 | title: string; | ||
| 25 | content: string; | ||
| 26 | creator: UserShort; | ||
| 27 | comments: MapDiscussionDetailComment[]; | ||
| 28 | created_at: string; | ||
| 29 | updated_at: string; | ||
| 30 | }; | ||
| 31 | |||
| 32 | interface MapDiscussionDetailComment { | ||
| 33 | comment: string; | ||
| 34 | date: string; | ||
| 35 | user: UserShort; | ||
| 36 | }; | ||
| 37 | |||
| 38 | export interface MapLeaderboard { | ||
| 39 | map: MapSummaryMap; | ||
| 40 | records: MapLeaderboardRecordSingleplayer[] | MapLeaderboardRecordMultiplayer[]; | ||
| 41 | pagination: Pagination; | ||
| 42 | }; | ||
| 43 | |||
| 44 | export interface MapLeaderboardRecordSingleplayer { | ||
| 45 | kind: "singleplayer"; | ||
| 46 | placement: number; | ||
| 47 | record_id: number; | ||
| 48 | score_count: number; | ||
| 49 | score_time: number; | ||
| 50 | user: UserShort; | ||
| 51 | demo_id: string; | ||
| 52 | record_date: string; | ||
| 53 | }; | ||
| 54 | |||
| 55 | export interface MapLeaderboardRecordMultiplayer { | ||
| 56 | kind: "multiplayer"; | ||
| 57 | placement: number; | ||
| 58 | record_id: number; | ||
| 59 | score_count: number; | ||
| 60 | score_time: number; | ||
| 61 | host: UserShort; | ||
| 62 | partner: UserShort; | ||
| 63 | host_demo_id: string; | ||
| 64 | partner_demo_id: string; | ||
| 65 | record_date: string; | ||
| 66 | }; | ||
| 67 | |||
| 68 | |||
| 69 | export interface MapSummary { | ||
| 70 | map: MapSummaryMap; | ||
| 71 | summary: MapSummaryDetails; | ||
| 72 | }; | ||
| 73 | |||
| 74 | interface MapSummaryMap { | ||
| 75 | id: number; | ||
| 76 | image: string; | ||
| 77 | chapter_name: string; | ||
| 78 | game_name: string; | ||
| 79 | map_name: string; | ||
| 80 | is_coop: boolean; | ||
| 81 | is_disabled: boolean; | ||
| 82 | }; | ||
| 83 | |||
| 84 | interface MapSummaryDetails { | ||
| 85 | routes: MapSummaryDetailsRoute[]; | ||
| 86 | }; | ||
| 87 | |||
| 88 | interface MapSummaryDetailsRoute { | ||
| 89 | route_id: number; | ||
| 90 | category: Category; | ||
| 91 | history: MapSummaryDetailsRouteHistory; | ||
| 92 | rating: number; | ||
| 93 | completion_count: number; | ||
| 94 | description: string; | ||
| 95 | showcase: string; | ||
| 96 | }; | ||
| 97 | |||
| 98 | interface MapSummaryDetailsRouteHistory { | ||
| 99 | runner_name: string; | ||
| 100 | score_count: number; | ||
| 101 | date: string; | ||
| 102 | }; | ||
| 103 | |||
diff --git a/frontend/src/types/Pagination.tsx b/frontend/src/types/Pagination.tsx new file mode 100644 index 0000000..ccff04b --- /dev/null +++ b/frontend/src/types/Pagination.tsx | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | export interface Pagination { | ||
| 2 | total_records: number; | ||
| 3 | total_pages: number; | ||
| 4 | current_page: number; | ||
| 5 | page_size: number; | ||
| 6 | }; | ||
diff --git a/frontend/src/types/Profile.tsx b/frontend/src/types/Profile.tsx new file mode 100644 index 0000000..2bb037c --- /dev/null +++ b/frontend/src/types/Profile.tsx | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | import { Pagination } from "./Pagination"; | ||
| 2 | |||
| 3 | export interface UserShort { | ||
| 4 | steam_id: string; | ||
| 5 | user_name: string; | ||
| 6 | avatar_link: string; | ||
| 7 | }; | ||
| 8 | |||
| 9 | export interface UserProfile { | ||
| 10 | profile: boolean; | ||
| 11 | steam_id: string; | ||
| 12 | user_name: string; | ||
| 13 | avatar_link: string; | ||
| 14 | country_code: string; | ||
| 15 | titles: UserProfileTitles[]; | ||
| 16 | links: UserProfileLinks; | ||
| 17 | rankings: UserProfileRankings; | ||
| 18 | records: UserProfileRecords[]; | ||
| 19 | pagination: Pagination; | ||
| 20 | }; | ||
| 21 | |||
| 22 | interface UserProfileTitles { | ||
| 23 | name: string; | ||
| 24 | color: string; | ||
| 25 | }; | ||
| 26 | |||
| 27 | interface UserProfileLinks { | ||
| 28 | p2sr: string; | ||
| 29 | steam: string; | ||
| 30 | youtube: string; | ||
| 31 | twitch: string; | ||
| 32 | }; | ||
| 33 | |||
| 34 | interface UserProfileRankings { | ||
| 35 | overall: UserProfileRankingsDetail; | ||
| 36 | singleplayer: UserProfileRankingsDetail; | ||
| 37 | cooperative: UserProfileRankingsDetail; | ||
| 38 | }; | ||
| 39 | |||
| 40 | interface UserProfileRecords { | ||
| 41 | game_id: number; | ||
| 42 | category_id: number; | ||
| 43 | map_id: number; | ||
| 44 | map_name: string; | ||
| 45 | map_wr_count: number; | ||
| 46 | placement: number; | ||
| 47 | scores: UserProfileRecordsScores[] | ||
| 48 | }; | ||
| 49 | |||
| 50 | interface UserProfileRecordsScores { | ||
| 51 | record_id: number; | ||
| 52 | demo_id: string; | ||
| 53 | score_count: number; | ||
| 54 | score_time: number; | ||
| 55 | date: string; | ||
| 56 | }; | ||
| 57 | |||
| 58 | interface UserProfileRankingsDetail { | ||
| 59 | rank: number; | ||
| 60 | completion_count: number; | ||
| 61 | completion_total: number; | ||
| 62 | }; | ||
| 63 | |||
diff --git a/frontend/src/types/Search.tsx b/frontend/src/types/Search.tsx new file mode 100644 index 0000000..766311a --- /dev/null +++ b/frontend/src/types/Search.tsx | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | import { UserShort } from "./Profile"; | ||
| 2 | |||
| 3 | export interface Search { | ||
| 4 | players: UserShort[]; | ||
| 5 | maps: SearchMap[]; | ||
| 6 | }; | ||
| 7 | |||
| 8 | interface SearchMap { | ||
| 9 | id: number; | ||
| 10 | game: string; | ||
| 11 | chapter: string; | ||
| 12 | map: string; | ||
| 13 | }; | ||
diff --git a/frontend/src/utils/Time.tsx b/frontend/src/utils/Time.tsx new file mode 100644 index 0000000..b83a7ed --- /dev/null +++ b/frontend/src/utils/Time.tsx | |||
| @@ -0,0 +1,42 @@ | |||
| 1 | export function time_ago(date: any) { | ||
| 2 | const now = new Date().getTime(); | ||
| 3 | |||
| 4 | const localDate = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); | ||
| 5 | const seconds = Math.floor((now - localDate.getTime()) / 1000); | ||
| 6 | |||
| 7 | let interval = Math.floor(seconds / 31536000); | ||
| 8 | if (interval === 1) {return interval + ' year ago';} | ||
| 9 | if (interval > 1) {return interval + ' years ago';} | ||
| 10 | |||
| 11 | interval = Math.floor(seconds / 2592000); | ||
| 12 | if (interval === 1) {return interval + ' month ago';} | ||
| 13 | if (interval > 1) {return interval + ' months ago';} | ||
| 14 | |||
| 15 | interval = Math.floor(seconds / 86400); | ||
| 16 | if (interval === 1) {return interval + ' day ago';} | ||
| 17 | if (interval > 1) {return interval + ' days ago';} | ||
| 18 | |||
| 19 | interval = Math.floor(seconds / 3600); | ||
| 20 | if (interval === 1) {return interval + ' hour ago';} | ||
| 21 | if (interval > 1) {return interval + ' hours ago';} | ||
| 22 | |||
| 23 | interval = Math.floor(seconds / 60); | ||
| 24 | if (interval === 1) {return interval + ' minute ago';} | ||
| 25 | if (interval > 1) {return interval + ' minutes ago';} | ||
| 26 | |||
| 27 | if(seconds < 10) return 'just now'; | ||
| 28 | |||
| 29 | return Math.floor(seconds) + ' seconds ago'; | ||
| 30 | }; | ||
| 31 | |||
| 32 | export function ticks_to_time(ticks: number) { | ||
| 33 | let seconds = Math.floor(ticks / 60) | ||
| 34 | let minutes = Math.floor(seconds / 60) | ||
| 35 | let hours = Math.floor(minutes / 60) | ||
| 36 | |||
| 37 | let milliseconds = Math.floor((ticks % 60) * 1000 / 60) | ||
| 38 | seconds = seconds % 60; | ||
| 39 | minutes = minutes % 60; | ||
| 40 | |||
| 41 | return `${hours === 0 ? "" : hours + ":"}${minutes === 0 ? "" : hours > 0 ? minutes.toString().padStart(2, '0') + ":" : (minutes + ":")}${minutes > 0 ? seconds.toString().padStart(2, '0') : seconds}.${milliseconds.toString().padStart(3, '0')}`; | ||
| 42 | }; \ No newline at end of file | ||