aboutsummaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
authorArda Serdar Pektezol <1669855+pektezol@users.noreply.github.com>2024-09-03 00:08:53 +0300
committerArda Serdar Pektezol <1669855+pektezol@users.noreply.github.com>2024-09-03 00:08:53 +0300
commita65d6d9127c3fa7f6a8ecaec5d1ffd1f47c2bc98 (patch)
treeedf8630e9d6426124dd49854af0cb703ebc5b710 /frontend/src
parentfix: revert to static homepage (#195) (diff)
downloadlphub-a65d6d9127c3fa7f6a8ecaec5d1ffd1f47c2bc98.tar.gz
lphub-a65d6d9127c3fa7f6a8ecaec5d1ffd1f47c2bc98.tar.bz2
lphub-a65d6d9127c3fa7f6a8ecaec5d1ffd1f47c2bc98.zip
refactor: port to typescript
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/App.css17
-rw-r--r--frontend/src/App.js49
-rw-r--r--frontend/src/App.tsx40
-rw-r--r--frontend/src/api/Api.tsx157
-rw-r--r--frontend/src/components/Discussions.tsx151
-rw-r--r--frontend/src/components/GameEntry.tsx49
-rw-r--r--frontend/src/components/Leaderboards.tsx105
-rw-r--r--frontend/src/components/Login.tsx1931
-rw-r--r--frontend/src/components/ModMenu.tsx324
-rw-r--r--frontend/src/components/Sidebar.tsx183
-rw-r--r--frontend/src/components/Summary.tsx169
-rw-r--r--frontend/src/components/login.js61
-rw-r--r--frontend/src/components/main.css17
-rw-r--r--frontend/src/components/main.js17
-rw-r--r--frontend/src/components/news.css29
-rw-r--r--frontend/src/components/news.js21
-rw-r--r--frontend/src/components/pages/about.css17
-rw-r--r--frontend/src/components/pages/about.js32
-rw-r--r--frontend/src/components/pages/game.js46
-rw-r--r--frontend/src/components/pages/games.js62
-rw-r--r--frontend/src/components/pages/home.css92
-rw-r--r--frontend/src/components/pages/home.js242
-rw-r--r--frontend/src/components/pages/maplist.css403
-rw-r--r--frontend/src/components/pages/maplist.js890
-rw-r--r--frontend/src/components/pages/profile.js382
-rw-r--r--frontend/src/components/pages/summary.js650
-rw-r--r--frontend/src/components/pages/summary_modview.js254
-rw-r--r--frontend/src/components/record.css15
-rw-r--r--frontend/src/components/record.js56
-rw-r--r--frontend/src/components/sidebar.js203
-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.tsx44
-rw-r--r--frontend/src/images/png/1.png (renamed from frontend/src/imgs/1.png)bin2011 -> 2011 bytes
-rw-r--r--frontend/src/images/png/10.png (renamed from frontend/src/imgs/10.png)bin1601 -> 1601 bytes
-rw-r--r--frontend/src/images/png/11.png (renamed from frontend/src/imgs/11.png)bin1294 -> 1294 bytes
-rw-r--r--frontend/src/images/png/12.png (renamed from frontend/src/imgs/12.png)bin1545 -> 1545 bytes
-rw-r--r--frontend/src/images/png/13.png (renamed from frontend/src/imgs/13.png)bin1251 -> 1251 bytes
-rw-r--r--frontend/src/images/png/14.png (renamed from frontend/src/imgs/14.png)bin1363 -> 1363 bytes
-rw-r--r--frontend/src/images/png/15.png (renamed from frontend/src/imgs/15.png)bin2988 -> 2988 bytes
-rw-r--r--frontend/src/images/png/16.png (renamed from frontend/src/imgs/16.png)bin3078 -> 3078 bytes
-rw-r--r--frontend/src/images/png/17.png (renamed from frontend/src/imgs/17.png)bin4943 -> 4943 bytes
-rw-r--r--frontend/src/images/png/18.png (renamed from frontend/src/imgs/18.png)bin2434 -> 2434 bytes
-rw-r--r--frontend/src/images/png/19.png (renamed from frontend/src/imgs/19.png)bin1266 -> 1266 bytes
-rw-r--r--frontend/src/images/png/2.png (renamed from frontend/src/imgs/2.png)bin1833 -> 1833 bytes
-rw-r--r--frontend/src/images/png/3.png (renamed from frontend/src/imgs/3.png)bin1517 -> 1517 bytes
-rw-r--r--frontend/src/images/png/4.png (renamed from frontend/src/imgs/4.png)bin4517 -> 4517 bytes
-rw-r--r--frontend/src/images/png/5.png (renamed from frontend/src/imgs/5.png)bin4112 -> 4112 bytes
-rw-r--r--frontend/src/images/png/6.png (renamed from frontend/src/imgs/6.png)bin2715 -> 2715 bytes
-rw-r--r--frontend/src/images/png/7.png (renamed from frontend/src/imgs/7.png)bin1608 -> 1608 bytes
-rw-r--r--frontend/src/images/png/8.png (renamed from frontend/src/imgs/8.png)bin1584 -> 1584 bytes
-rw-r--r--frontend/src/images/png/9.png (renamed from frontend/src/imgs/9.png)bin6037 -> 6037 bytes
-rw-r--r--frontend/src/images/png/login.png (renamed from frontend/src/imgs/login.png)bin4871 -> 4871 bytes
-rw-r--r--frontend/src/images/png/logo.png (renamed from frontend/src/imgs/logo.png)bin67124 -> 67124 bytes
-rw-r--r--frontend/src/index.js8
-rw-r--r--frontend/src/index.tsx17
-rw-r--r--frontend/src/pages/Games.tsx51
-rw-r--r--frontend/src/pages/Maps.tsx91
-rw-r--r--frontend/src/pages/Profile.tsx326
-rw-r--r--frontend/src/pages/User.tsx320
-rw-r--r--frontend/src/react-app-env.d.ts2
-rw-r--r--frontend/src/types/Content.tsx18
-rw-r--r--frontend/src/types/Game.tsx37
-rw-r--r--frontend/src/types/Map.tsx103
-rw-r--r--frontend/src/types/Pagination.tsx6
-rw-r--r--frontend/src/types/Profile.tsx63
-rw-r--r--frontend/src/types/Search.tsx13
-rw-r--r--frontend/src/utils/Time.tsx42
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 @@
1main {
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
1body { 18body {
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 @@
1import React from 'react';
2import { BrowserRouter, Routes, Route} from "react-router-dom";
3
4import Sidebar from "./components/sidebar.js"
5import Main from "./components/main.js"
6import "./App.css";
7
8import Summary from "./components/pages/summary.js"
9import Profile from "./components/pages/profile.js"
10import About from './components/pages/about.js';
11import Games from "./components/pages/games.js";
12import Maplist from './components/pages/maplist.js';
13import Home from "./components/pages/maplist.js";
14import Homepage from './components/pages/home.js';
15
16
17export 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 @@
1import React from 'react';
2import { Routes, Route } from "react-router-dom";
3
4import { UserProfile } from './types/Profile';
5import Sidebar from './components/Sidebar';
6import "./App.css";
7
8import Profile from './pages/Profile';
9import Games from './pages/Games';
10import Maps from './pages/Maps';
11import User from './pages/User';
12
13
14const 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
40export 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 @@
1import axios from 'axios';
2
3import { Game } from '../types/Game';
4import { MapDiscussion, MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map';
5import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '../types/Content';
6import { Search } from '../types/Search';
7import { UserProfile } from '../types/Profile';
8
9// add new api call function entries here
10// example usage: API.get_games();
11export 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
33const BASE_API_URL: string = "https://lp.ardapektezol.com/api/v1/"
34
35function url(path: string): string {
36 return BASE_API_URL + path;
37}
38
39// USER
40
41const user_logout = async () => {
42 await axios.delete(url("token"));
43};
44
45const 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
53const get_games = async (): Promise<Game[]> => {
54 const response = await axios.get(url("games"))
55 return response.data.data;
56};
57
58// SEARCH
59
60const 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
67const 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
74const 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
79const 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
90const 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
102const 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
113const 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
123const 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
131const 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
139const 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
147const 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
154const 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 @@
1import React from 'react';
2
3import { MapDiscussion, MapDiscussions, MapDiscussionsDetail } from '../types/Map';
4import { MapDiscussionCommentContent, MapDiscussionContent } from '../types/Content';
5import { time_ago } from '../utils/Time';
6import { API } from '../api/Api';
7import "../css/Maps.css"
8
9interface DiscussionsProps {
10 data?: MapDiscussions;
11 isModerator: boolean;
12 mapID: string;
13 onRefresh: () => void;
14}
15
16const 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
151export 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 @@
1import React from 'react';
2import { Link } from "react-router-dom";
3
4import { Game } from '../types/Game';
5import "../css/Games.css"
6
7interface GameEntryProps {
8 game: Game;
9}
10
11const 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
49export 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 @@
1import React from 'react';
2
3import { DownloadIcon, ThreedotIcon } from '../images/Images';
4import { MapLeaderboard } from '../types/Map';
5import { ticks_to_time, time_ago } from '../utils/Time';
6import "../css/Maps.css"
7
8interface LeaderboardsProps {
9 data?: MapLeaderboard;
10}
11
12const 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='' /> &nbsp; {r.host.user_name}</span>
74 <span><img src={r.partner.avatar_link} alt='' /> &nbsp; {r.partner.user_name}</span>
75 </div>
76 ) : (
77 <div><span><img src={r.user.avatar_link} alt='' /> &nbsp; {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
105export 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 @@
1import React from 'react';
2import { Link, useNavigate } from 'react-router-dom';
3
4import { ExitIcon, UserIcon, LoginIcon } from '../images/Images';
5import { UserProfile } from '../types/Profile';
6import { API } from '../api/Api';
7import "../css/Login.css";
8
9interface 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
16const 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
1931export 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 @@
1import React from 'react';
2import ReactMarkdown from 'react-markdown';
3
4import { MapSummary } from '../types/Map';
5import { ModMenuContent } from '../types/Content';
6import { API } from '../api/Api';
7import "../css/ModMenu.css"
8
9interface ModMenuProps {
10 data: MapSummary;
11 selectedRun: number;
12 mapID: string;
13}
14
15const 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
324export 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 @@
1import React from 'react';
2import { Link, useLocation } from 'react-router-dom';
3
4import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, NewsIcon, PortalIcon, SearchIcon, TableIcon } from '../images/Images';
5import Login from './Login';
6import { UserProfile } from '../types/Profile';
7import { Search } from '../types/Search';
8import { API } from '../api/Api';
9import "../css/Sidebar.css";
10
11interface SidebarProps {
12 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
13 profile?: UserProfile;
14 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
15};
16
17const 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&nbsp;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&nbsp;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&nbsp;Rules</span></button>
147 </Link>
148
149 <Link to="/about" tabIndex={-1}>
150 <button className='sidebar-button'><img src={HelpIcon} alt="aboutp2lp" /><span>About&nbsp;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
183export 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 @@
1import React from 'react';
2import ReactMarkdown from 'react-markdown';
3
4import { MapSummary } from '../types/Map';
5import "../css/Maps.css"
6
7interface SummaryProps {
8 selectedRun: number
9 setSelectedRun: (x: number) => void;
10 data: MapSummary;
11}
12
13const 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
169export 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 @@
1import React from 'react';
2import { Link } from "react-router-dom";
3
4import "./login.css";
5import img1 from "../imgs/login.png"
6import img2 from "../imgs/10.png"
7import img3 from "../imgs/11.png"
8
9
10export default function Login(prop) {
11const {setToken,profile,setProfile} = prop
12function login() {
13 window.location.href="https://lp.ardapektezol.com/api/v1/login"
14}
15function 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}
22const [isLoggedIn, setIsLoggedIn] = React.useState(false);
23React.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
34React.useEffect(() => {
35 if(profile!==null){setIsLoggedIn(true)}
36 }, [profile]);
37
38return (
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
2main {
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 @@
1import React from 'react';
2
3import "../App.css"
4import "./main.css";
5import { Link } from 'react-router-dom';
6
7export default function Main(props) {
8
9
10return (
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 @@
1import React, { useEffect, useRef, useState } from 'react';
2import { useLocation, Link } from "react-router-dom";
3
4import "./news.css"
5
6export 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 @@
1import React, { useState, useEffect } from 'react';
2import ReactMarkdown from 'react-markdown';
3
4import "./about.css";
5
6export 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 @@
1import React, { useEffect, useRef, useState } from 'react';
2import { useLocation, Link } from "react-router-dom";
3
4import "./games.css"
5
6export 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 @@
1import React, { useEffect, useState } from 'react';
2import { useLocation, Link } from "react-router-dom";
3
4import "./games.css"
5import GameEntry from './game';
6
7export 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 @@
1import React, { useEffect, useState } from 'react';
2
3import "./home.css"
4import News from '../news';
5import Record from '../record';
6
7export 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
72const 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
131return (
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>&nbsp;{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>&nbsp;{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
13a {
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 @@
1import React, { useEffect, useRef, useState } from 'react';
2import { useLocation, Link } from "react-router-dom";
3import { BrowserRouter as Router, Route, Routes, useNavigate } from 'react-router-dom';
4
5import "./maplist.css"
6import img5 from "../../imgs/5.png"
7import img6 from "../../imgs/6.png"
8
9export 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 @@
1import React from 'react';
2import { useLocation } from "react-router-dom";
3
4import img4 from "../../imgs/4.png"
5import img5 from "../../imgs/5.png"
6import img12 from "../../imgs/12.png"
7import img13 from "../../imgs/13.png"
8import img14 from "../../imgs/14.png"
9import img15 from "../../imgs/15.png"
10import img16 from "../../imgs/16.png"
11import img17 from "../../imgs/17.png"
12import img18 from "../../imgs/18.png"
13import img19 from "../../imgs/19.png"
14import "./profile.css";
15
16export default function Profile(props) {
17const {token} = props
18
19
20const location = useLocation()
21
22
23const [profileData, setProfileData] = React.useState(null)
24React.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
52const [game,setGame] = React.useState(0)
53const [gameData,setGameData] = React.useState(null)
54const [chapter,setChapter] = React.useState("0")
55const [chapterData,setChapterData] = React.useState(null)
56const [maps,setMaps] = React.useState(null)
57
58React.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
68React.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
85React.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
112const [pageNumber, setPageNumber] = React.useState(1);
113const [pageMax, setPageMax] = React.useState(0);
114const [navState, setNavState] = React.useState(0); // eslint-disable-next-line
115React.useEffect(() => {NavClick();}, [[],navState]);
116function 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}
127function 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
135function 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
158function 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
172if(profileData!==null){
173return (
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="" />&nbsp;Player Records</button>
235 <button onClick={()=>setNavState(1)}><img src={img14} alt="" />&nbsp;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 @@
1import React, { useEffect } from 'react';
2import { useLocation, Link } from "react-router-dom";
3import ReactMarkdown from 'react-markdown'
4
5import "./summary.css";
6
7import img4 from "../../imgs/4.png"
8import img5 from "../../imgs/5.png"
9import img6 from "../../imgs/6.png"
10import img12 from "../../imgs/12.png"
11import img13 from "../../imgs/13.png"
12import Modview from "./summary_modview.js"
13
14export default function Summary(prop) {
15const {token,mod} = prop
16const 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
60const [discussionThread,setDiscussionThread] = React.useState(null)
61function 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}
66const [discussionSearch, setDiscussionSearch] = React.useState("")
67
68
69
70
71const [navState, setNavState] = React.useState(0); // eslint-disable-next-line
72React.useEffect(() => {NavClick();}, [[],navState]);
73
74function 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
87const [catState, setCatState] = React.useState(1); // eslint-disable-next-line
88React.useEffect(() => {CatClick();}, [[],catState]);
89
90function 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}}
96React.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
102const [hisState, setHisState] = React.useState(0); // eslint-disable-next-line
103React.useEffect(() => {HisClick();}, [[],hisState]);
104
105function 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
113const [selectedRun,setSelectedRun] = React.useState(0)
114
115function 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
129function 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
218const [vid,setVid] = React.useState("")
219React.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
227function 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
232function 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
262function 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
275function 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
290const [createPostState,setCreatePostState] = React.useState(0)
291function 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
305function DeletePost(post) {
306if(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
320if(data!==null){
321 console.log(data)
322
323let current_chapter = data.map.chapter_name
324let isCoop = false;
325if (data.map.game_name == "Portal 2 - Cooperative") {
326 isCoop = true
327}
328
329current_chapter = data.map.chapter_name.split(" ")
330// current_chapter = current_chapter.split("-")
331current_chapter = current_chapter[1]
332
333return (
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='' /> &nbsp; {r.host.user_name}</span>
510 <span><img src={r.partner.avatar_link} alt='' /> &nbsp; {r.partner.user_name}</span>
511 </div>
512 ):(
513 <div><span><img src={r.user.avatar_link} alt='' /> &nbsp; {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 @@
1import React from 'react';
2import { useLocation } from "react-router-dom";
3import ReactMarkdown from 'react-markdown'
4
5import "./summary_modview.css";
6
7
8export default function Modview(prop) {
9const {selectedRun,data,token} = prop
10
11const [menu,setMenu] = React.useState(0)
12React.useEffect(()=>{
13if(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 }
20if(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
30function 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}
56const [image,setImage] = React.useState(null)
57function 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}
66const location = useLocation()
67function editRoute(){
68if(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
86function 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
104function deleteRoute(){
105if(data.summary.routes[0].category==='')
106{window.alert("no run selected")}else{
107if(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
119const [showButton, setShowButton] = React.useState(1)
120const modview = document.querySelector("div#modview")
121React.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
130const [md,setMd] = React.useState("")
131
132return (
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 @@
1import React, { useEffect, useRef, useState } from 'react';
2import { useLocation, Link } from "react-router-dom";
3
4import "./record.css"
5
6export 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 @@
1import React, { useEffect } from 'react';
2import { Link, useLocation } from "react-router-dom";
3
4import "../App.css"
5import "./sidebar.css";
6import logo from "../imgs/logo.png"
7import img1 from "../imgs/1.png"
8import img2 from "../imgs/2.png"
9import img3 from "../imgs/3.png"
10import img4 from "../imgs/4.png"
11import img5 from "../imgs/5.png"
12import img6 from "../imgs/6.png"
13import img7 from "../imgs/7.png"
14import img8 from "../imgs/8.png"
15import img9 from "../imgs/9.png"
16import Login from "./login.js"
17
18export default function Sidebar(prop) {
19const {token,setToken} = prop
20const [profile, setProfile] = React.useState(null);
21
22React.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
33const [isLocked, setIsLocked] = React.useState(false);
34function HandleLock(arg) {
35if (!isLocked) {
36 setIsLocked(true);
37 setTimeout(() => setIsLocked(false), 300);
38 SidebarHide(arg)
39 }
40}
41
42
43// The menu button
44const [sidebar, setSidebar] = React.useState();
45
46// Clicked buttons
47function SidebarClick(x){
48const btn = document.querySelectorAll("button.sidebar-button");
49
50if(sidebar===1){setSidebar(0);SidebarHide()}
51
52// clusterfuck
53btn.forEach((e,i) =>{
54 btn[i].classList.remove("sidebar-button-selected")
55 btn[i].classList.add("sidebar-button-deselected")
56})
57btn[x].classList.add("sidebar-button-selected")
58btn[x].classList.remove("sidebar-button-deselected")
59
60}
61
62function SidebarHide(){
63const btn = document.querySelectorAll("button.sidebar-button")
64const span = document.querySelectorAll("button.sidebar-button>span");
65const side = document.querySelector("#sidebar-list");
66const login = document.querySelectorAll(".login>button")[1];
67const searchbar = document.querySelector("#searchbar");
68
69if(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
99const location = useLocation()
100React.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
113const [search,setSearch] = React.useState(null)
114const [searchData,setSearchData] = React.useState(null)
115
116React.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
124return (
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&nbsp;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&nbsp;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&nbsp;Rules</span></button>
167 </Link>
168
169 <Link to="/about" tabIndex={-1}>
170 <button className='sidebar-button'><img src={img9} alt="" /><span>About&nbsp;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 @@
1import logo from "./png/logo.png"
2import login from "./png/login.png"
3import img1 from './png/1.png';
4import img2 from './png/2.png';
5import img3 from './png/3.png';
6import img4 from './png/4.png';
7import img5 from './png/5.png';
8import img6 from './png/6.png';
9import img7 from './png/7.png';
10import img8 from './png/8.png';
11import img9 from './png/9.png';
12import img10 from './png/10.png';
13import img11 from './png/11.png';
14import img12 from './png/12.png';
15import img13 from './png/13.png';
16import img14 from './png/14.png';
17import img15 from './png/15.png';
18import img16 from './png/16.png';
19import img17 from './png/17.png';
20import img18 from './png/18.png';
21import img19 from './png/19.png';
22
23export const LogoIcon = logo;
24export const LoginIcon = login;
25
26export const SearchIcon = img1;
27export const HomeIcon = img2;
28export const NewsIcon = img3;
29export const PortalIcon = img4;
30export const FlagIcon = img5;
31export const ChatIcon = img6;
32export const TableIcon = img7;
33export const BookIcon = img8;
34export const HelpIcon = img9;
35export const UserIcon = img10;
36export const ExitIcon = img11;
37export const DownloadIcon = img12;
38export const ThreedotIcon = img13;
39export const StatisticsIcon = img14;
40export const TwitchIcon = img15;
41export const YouTubeIcon = img16;
42export const SteamIcon = img17;
43export const HistoryIcon = img18;
44export 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 @@
1import React from 'react';
2import ReactDOM from 'react-dom/client';
3import App from './App';
4
5const root = ReactDOM.createRoot(document.getElementById('root'));
6root.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 @@
1import React from 'react';
2import ReactDOM from 'react-dom/client';
3import { BrowserRouter } from "react-router-dom";
4
5import App from './App';
6
7const root = ReactDOM.createRoot(
8 document.getElementById('root') as HTMLElement
9);
10
11root.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 @@
1import React from 'react';
2
3import GameEntry from '../components/GameEntry';
4import { Game } from '../types/Game';
5import { API } from '../api/Api';
6import "../css/Maps.css"
7
8const 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
51export 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 @@
1import React from 'react';
2import { Link, useLocation } from 'react-router-dom';
3
4import { PortalIcon, FlagIcon, ChatIcon } from '../images/Images';
5import Summary from '../components/Summary';
6import Leaderboards from '../components/Leaderboards';
7import Discussions from '../components/Discussions';
8import ModMenu from '../components/ModMenu';
9import { MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map';
10import { API } from '../api/Api';
11import "../css/Maps.css";
12
13interface MapProps {
14 isModerator: boolean;
15};
16
17const 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
91export 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 @@
1import React from 'react';
2import { useLocation, useNavigate } from 'react-router-dom';
3
4import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '../images/Images';
5import { UserProfile } from '../types/Profile';
6import { Game, GameChapters } from '../types/Game';
7import { Map } from '../types/Map';
8import "../css/Profile.css";
9
10interface ProfileProps {
11 profile: UserProfile;
12}
13
14const 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="" />&nbsp;Player Records</button>
182 <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" />&nbsp;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
326export 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 @@
1import React from 'react';
2import { useLocation } from 'react-router-dom';
3
4import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '../images/Images';
5import { UserProfile } from '../types/Profile';
6import { Game, GameChapters } from '../types/Game';
7import { Map } from '../types/Map';
8import { API } from '../api/Api';
9import { ticks_to_time } from '../utils/Time';
10import "../css/Profile.css";
11
12const 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="" />&nbsp;Player Records</button>
176 <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" />&nbsp;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
320export 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 @@
1declare module "*.png";
2declare 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 @@
1export 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
11export interface MapDiscussionContent {
12 title: string;
13 content: string;
14};
15
16export 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 @@
1import { Map } from './Map';
2
3
4export interface Game {
5 id: number;
6 name: string;
7 image: string;
8 is_coop: boolean;
9 category_portals: GameCategoryPortals[];
10};
11
12export interface GameChapters {
13 game: Game;
14 chapters: Chapter[];
15};
16
17export interface GameMaps {
18 game: Game;
19 maps: Map[];
20};
21
22export interface Category {
23 id: number;
24 name: string;
25};
26
27interface Chapter {
28 id: number;
29 name: string;
30 image: string;
31 is_disabled: boolean;
32};
33
34export 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 @@
1import { Category, GameCategoryPortals } from './Game';
2import { Pagination } from './Pagination';
3import { UserShort } from './Profile';
4
5export interface Map {
6 id: number;
7 name: string;
8 image: string;
9 is_disabled: boolean;
10 difficulty: number;
11 category_portals: GameCategoryPortals[];
12};
13
14export interface MapDiscussion {
15 discussion: MapDiscussionsDetail;
16};
17
18export interface MapDiscussions {
19 discussions: MapDiscussionsDetail[];
20};
21
22export 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
32interface MapDiscussionDetailComment {
33 comment: string;
34 date: string;
35 user: UserShort;
36};
37
38export interface MapLeaderboard {
39 map: MapSummaryMap;
40 records: MapLeaderboardRecordSingleplayer[] | MapLeaderboardRecordMultiplayer[];
41 pagination: Pagination;
42};
43
44export 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
55export 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
69export interface MapSummary {
70 map: MapSummaryMap;
71 summary: MapSummaryDetails;
72};
73
74interface 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
84interface MapSummaryDetails {
85 routes: MapSummaryDetailsRoute[];
86};
87
88interface 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
98interface 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 @@
1export 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 @@
1import { Pagination } from "./Pagination";
2
3export interface UserShort {
4 steam_id: string;
5 user_name: string;
6 avatar_link: string;
7};
8
9export 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
22interface UserProfileTitles {
23 name: string;
24 color: string;
25};
26
27interface UserProfileLinks {
28 p2sr: string;
29 steam: string;
30 youtube: string;
31 twitch: string;
32};
33
34interface UserProfileRankings {
35 overall: UserProfileRankingsDetail;
36 singleplayer: UserProfileRankingsDetail;
37 cooperative: UserProfileRankingsDetail;
38};
39
40interface 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
50interface UserProfileRecordsScores {
51 record_id: number;
52 demo_id: string;
53 score_count: number;
54 score_time: number;
55 date: string;
56};
57
58interface 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 @@
1import { UserShort } from "./Profile";
2
3export interface Search {
4 players: UserShort[];
5 maps: SearchMap[];
6};
7
8interface 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 @@
1export 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
32export 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