aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components
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/components
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/components')
-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.css26
-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.css99
-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.css239
-rw-r--r--frontend/src/components/pages/profile.js382
-rw-r--r--frontend/src/components/pages/summary.css720
-rw-r--r--frontend/src/components/pages/summary.js650
-rw-r--r--frontend/src/components/pages/summary_modview.css112
-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.css208
-rw-r--r--frontend/src/components/sidebar.js203
32 files changed, 2912 insertions, 4893 deletions
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.css b/frontend/src/components/login.css
deleted file mode 100644
index b46be10..0000000
--- a/frontend/src/components/login.css
+++ /dev/null
@@ -1,26 +0,0 @@
1span>img {
2 scale: 1.1;
3 padding: 4px 0 0 8px;
4}
5.login>button>span{
6 max-width: 22ch;
7 overflow: hidden;
8}
9.login>button:nth-child(2){
10 position: relative;
11 left: 210px;
12 width: 50px !important;
13
14 padding-left: 10px;
15 background-color: #00000000 !important;
16 transition: opacity .1s;
17}
18
19.login{
20 display: grid;
21 grid-template-columns: 50px auto 200px ;
22}
23
24button:disabled {
25 display: none;
26} \ No newline at end of file
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.css b/frontend/src/components/pages/games.css
deleted file mode 100644
index ec57a71..0000000
--- a/frontend/src/components/pages/games.css
+++ /dev/null
@@ -1,99 +0,0 @@
1.games-page {
2 position: absolute;
3 left: 320px;
4 color: white;
5 width: calc(100% - 320px);
6 height: 100%;
7 font-family: BarlowSemiCondensed-Regular;
8 color: #ffffff;
9 overflow-y: scroll;
10 scrollbar-width: thin;
11}
12
13.games-page-item-content {
14 position: absolute;
15 left: 50px;
16 width: calc(100% - 100px);
17}
18
19.games-page-item-content a {
20 color: inherit;
21}
22
23.games-page-header {
24 margin-top: 50px;
25 margin-left: 50px;
26}
27
28span>b {
29 font-size: 56px;
30 font-family: BarlowCondensed-Bold;
31}
32
33.loader-game {
34 width: 100%;
35 height: 256px;
36 border-radius: 24px;
37 overflow: hidden;
38 margin: 25px 0px;
39}
40
41.games-page-item {
42 width: 100%;
43 height: 256px;
44 background: #202232;
45 border-radius: 24px;
46 overflow: hidden;
47 margin: 25px 0px;
48}
49
50.games-page-item-header {
51 width: 100%;
52 height: 50%;
53 background-size: cover;
54 overflow: hidden;
55}
56
57.games-page-item-header-img {
58 width: 100%;
59 height: 100%;
60 backdrop-filter: blur(4px);
61 filter: blur(4px);
62 background-size: cover;
63}
64
65.games-page-item-header span>b {
66 display: flex;
67 justify-content: center;
68 align-items: center;
69 height: 100%;
70 transform: translateY(-100%);
71}
72
73.games-page-item-body {
74 display: flex;
75 justify-content: center;
76 align-items: center;
77 height: 50%;
78}
79
80.games-page-item-body-item {
81 background: #2B2E46;
82 text-align: center;
83 width: max-content;
84 width: calc(100% - 24px);
85 height: 100px;
86 border-radius: 24px;
87 color: #CDCFDF;
88 margin: 12px;
89}
90
91.games-page-item-body-item-title {
92 margin-top: 0px;
93 font-size: 26px;
94}
95
96.games-page-item-body-item-num {
97 font-size: 50px;
98 font-family: BarlowCondensed-Bold;
99} \ No newline at end of file
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.css b/frontend/src/components/pages/profile.css
deleted file mode 100644
index 4944ade..0000000
--- a/frontend/src/components/pages/profile.css
+++ /dev/null
@@ -1,239 +0,0 @@
1#section1.profile{
2 margin: 20px;
3 background: linear-gradient(0deg, #202232 50%, #2b2e46 50%);
4 border-radius: 24px;
5 height: 200px;
6
7 display: grid;
8 grid-template-columns: 250px 1fr;
9
10}
11#section1.profile>div:first-child{
12 overflow: hidden;
13 border-radius: 100%;
14 display: grid;
15
16 place-items: center;
17
18 margin: 8px 33px 8px 33px;
19 scale: 0.9;
20 grid-row: 1 / 3;
21
22
23}
24#profile-image>img{
25 border-radius: 100%;
26 transition: filter 0.3s;
27 cursor: pointer;
28}
29
30#profile-image>span{
31 z-index: 1;
32 position: absolute;
33 opacity: 0;
34 color:white;
35 transition: opacity 0.3s;
36 cursor: pointer;
37}
38
39#profile-image:hover > img{filter: blur(5px) brightness(60%);z-index: 1;}
40#profile-image:hover > span{opacity: 1;}
41
42#profile-top{
43 height: 100px;
44 display: grid;
45 grid-template-columns: 80% 20%;
46}
47#profile-top>div:nth-child(1)>div>img{
48 margin: 12px;
49 border-radius: 10px;
50}
51
52#profile-top>div:nth-child(1){
53 display: flex;
54 place-items: center;
55 font-size: 50px;
56 font-weight: bold;
57 color: white;
58}
59#profile-top>div:nth-child(1)>div{
60 display: flex;
61 height: 60px;
62}
63span.titles{
64 margin: 12px 12px 12px 0;
65
66 font-size: 18px;
67 font-weight: 100;
68
69 padding: 6px 20px 0px 20px;
70 border-radius: 10px;
71}
72
73#profile-top>div:nth-child(2){
74 display: flex;
75 flex-direction: row-reverse;
76 align-items: center;
77 padding-right: 10px;
78}
79#profile-top>div:nth-child(2)>a>img{
80 height: 50px;
81 padding: 0 5px 0 5px;
82 scale: 0.9;
83 filter: brightness(200%);
84
85}
86
87
88#profile-bottom{
89 height: 100px;
90 display: grid;
91 grid-template-columns: 1fr 1fr 1fr;
92}
93#profile-bottom>div{
94 margin: 12px;
95 background-color: #2b2e46;
96 border-radius: 20px;
97 display: grid;
98 place-items: center;
99 grid-template-rows: 40% 50%;
100}
101#profile-bottom>div>span:nth-child(1){
102 color: inherit;
103 font-size: 18px;
104}
105#profile-bottom>div>span:nth-child(2){
106 color: white;
107 font-size: 40px;
108}
109#profile-bottom>div>span:nth-child(2)>span{
110 color: white;
111 font-size: 20px;
112}
113/* #section1.profile>div>div{outline: red 1px dashed;} */
114
115
116#section2.profile{
117 margin: 20px;
118 height: 60px;
119 display: grid;
120 grid-template-columns: 1fr 1fr;
121}
122#section2.profile>button{
123 display: flex;
124 justify-content: center;
125 align-items: center;
126
127 background-color: #2b2e46;
128 border: 0;
129 color: inherit;
130 font-family: inherit;
131 font-size: 24px;
132 cursor: pointer;
133
134 transition: background-color .1s;
135}
136#section2.profile>button:nth-child(1){border-radius: 24px 0 0 24px;}
137#section2.profile>button:nth-child(2){border-radius: 0 24px 24px 0;}
138
139#section3.profile1>hr{border: 1px solid #2b2e46;margin: 8px 20px 0px 20px;}
140#section3.profile1{
141 margin: 20px;
142 display: block;
143
144 background-color: #202232;
145 border-radius: 24px;
146}
147
148#profileboard-nav{
149 display: grid;
150 grid-template-columns: 1fr 1fr;
151}
152
153#profileboard-nav>select{
154
155 /* appearance: none; */
156 margin: 10px 20px 20px 20px;
157 height: 50px;
158
159 border-radius: 24px;
160 text-align: center;
161
162 color: inherit;
163 font-family: inherit;
164 font-size: 24px;
165 border: 0;
166
167 background-color: #2b2e46;
168}
169
170
171#profileboard-top>span>img{height: 20px;scale: .8;}
172#profileboard-top>span>img,#profileboard-top>span>span{cursor: pointer;}
173#profileboard-top{
174 height: 34px;
175 display: grid;
176 font-size: 20px;
177 padding-left: 40px;
178 margin: 0 20px;
179 grid-template-columns: 15% 15% 5% 15% 5% 15% 15% 15%;
180}
181
182#profileboard-top>span{
183 display: flex;
184 place-items: flex-end;
185}
186
187#profileboard-records{
188 padding-bottom: 10px;
189}
190
191.profileboard-record{
192 width: calc(100% - 40px);
193 margin: 10px 20px 0px 20px;
194 height: 44px;
195
196 border-radius: 20px;
197 padding: 0 0 0 40px;
198 font-size: 20px;
199
200 color: inherit;
201 font-family: inherit;
202 border: 0;
203 transition: background-color .1s;
204 background-color: #2b2e46;
205 display: grid;
206 grid-template-columns: 15% 15% 5% 15% 5% 15% 15% 15%;
207 overflow: hidden;
208 white-space: nowrap;
209
210 transition: height .2s
211}
212
213/* this right here should be illegal */
214.profileboard-record>span:nth-child(-n+8){filter: brightness(100%);}
215.profileboard-record>span{
216 display: flex;
217 place-items: flex-end;
218 filter: brightness(65%);
219}
220
221.profileboard-record>hr{
222 margin: 0 0 0 -60px;
223 border: 0;
224 height: 2px;
225 background-color: #202232;
226}
227
228.profileboard-record>span:nth-child(4){display: grid;}
229.profileboard-record>span{
230
231 display: flex;
232 place-items: center;
233 height: 44px;
234}
235.profileboard-record>span>button{
236 background-color: #0000;
237 border: 0;
238 cursor: pointer;
239}
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.css b/frontend/src/components/pages/summary.css
deleted file mode 100644
index 8c6ec35..0000000
--- a/frontend/src/components/pages/summary.css
+++ /dev/null
@@ -1,720 +0,0 @@
1#background-image{
2 z-index: -1;
3 position: absolute;
4 opacity: 10%;
5 height: 50%;
6 width: 100%
7}
8#background-image>img{
9 object-fit: cover;
10 width: 100%;
11 height: 100%;
12}
13#background-image::before{
14 content: "";
15 position: absolute;
16 width: 100%;
17 height: 100%;
18 background: linear-gradient(to top, #161723, #0000);
19}
20
21/* Section 1: map name*/
22
23#section1{
24 margin: 20px 0 0 0;
25 cursor: default;
26}
27
28.nav-button{
29 height: 40px;
30 background-color: #2b2e46;
31
32 color: inherit;
33 font-size: 18px;
34 font-family: inherit;
35 border: none;
36 transition: background-color .1s;
37}
38/* #section1>div>.nav-button:nth-child(1){border-radius: 0px;}:nth-child(1){border-radius: 20px 0 0 20px;}
39#section1>div>.nav-button:nth-child(2){border-radius: 0 20px 20px 0;margin-left: 2px;} */
40.nav-button>span{padding: 0 8px 0 8px;}
41.nav-button:hover{background-color: #202232;cursor: pointer;}
42
43/* Section 2: navbar */
44#section2{
45 margin: 40px 0 0 0;
46
47 display: grid; gap: 2px;
48 grid-template-columns: 1fr 1fr 1fr;
49}
50
51#section2>.nav-button{
52 height: 50px;
53 font-size: 22px;
54 display: flex;
55 justify-content: center;
56 place-items: center;
57}
58#section2>.nav-button>img{scale: 1.2;}
59#section2>.nav-button:nth-child(1){border-radius: 30px 0 0 30px;}
60#section2>.nav-button:nth-child(3){border-radius: 0 30px 30px 0;}
61
62
63/* Section 3: category + history */
64
65#section3{
66 margin: 40px 0 0 0;
67
68 display: none;
69 grid-template-columns: 1fr 1fr;
70 gap: 20px;
71}
72
73#category{
74 display: grid;
75 height: 350px;
76 border-radius: 24px;
77 overflow: hidden;
78
79}
80#category>p{
81 margin-bottom: 20px;
82 text-align: center;
83 font-size: 50px;
84 cursor: default;
85 color: white;
86}
87
88p>span.portal-count{font-weight: bold;font-size: 100px;vertical-align: -15%;}
89
90#category-image{
91 transform: translate(-20%, -15%);
92 z-index: -1;
93 overflow: hidden;
94 width: 125%;
95 margin: 22px;
96 filter: blur(4px) contrast(80%) brightness(80%);
97}
98
99#category>span{
100 margin-top: 70px;
101 background-color: #202232;
102
103 display: grid;
104 grid-template-columns: 1fr 1fr 1fr 1fr;
105 gap: 2px;
106}
107#category>span>button{
108 font-family: inherit;
109 font-size: 18px;
110 border: none;
111 height: 40px;
112 color: inherit;
113
114 cursor: pointer;
115 transition: background-color .1s;
116}
117
118
119
120#history>div>hr{border: 1px solid #2b2e46;margin: 8px 20px 0px 20px;}
121#history{
122 min-width: 560px;
123 background-color: #202232;
124 border-radius: 24px;
125
126}
127
128#records{overflow-y: auto; height: 256px;}
129#records::-webkit-scrollbar{display: none;}
130
131.record-top, .record{
132 font-size: 18px;
133 display: grid;
134 text-align: center;
135 grid-template-columns: 1fr 1fr 1fr;
136}
137.record-top{font-weight: bold;margin: 20px 20px 0 20px;cursor: default;}
138.record{
139 margin: 10px 20px 10px 20px;
140 height: 44px; width: calc(100% - 40px);
141
142 color: inherit;
143 border-radius: 40px;
144 place-items: center;
145
146 border: 0;
147 cursor: pointer;
148 transition: background-color .1s;
149}
150#history>span{
151 border-top: #202232 solid 2px;
152 display: grid;
153 grid-template-columns: 1fr 1fr;
154}
155#history>span>button{
156 width: 100%; height: 40px;
157 font-family: inherit;
158 font-size: 18px;
159 border: none;
160 color: inherit;
161
162 cursor: pointer;
163 transition: background-color .1s;
164}
165#history>span>button:nth-child(1){border-radius: 0 0 0 24px;}
166#history>span>button:nth-child(2){border-radius: 0 0 24px 0;}
167
168#graph{
169 display: grid;
170 grid-template-columns: 20px 1fr;
171 grid-template-rows: 1fr 20px;
172 height: 293px;
173
174 margin: 10px 10px 5px 10px;
175 overflow: hidden;
176}
177#graph>div:nth-child(1){ /* numbers */
178 width: 20px;
179 display: grid;
180 place-items: center;
181 /* background-color: blue; */
182}
183#graph>div:nth-child(1)>span{
184 font-size: 12px;
185 line-height: 0;
186}
187
188#graph>div:nth-child(2){ /* big graph */
189 position: relative;
190 display: grid;
191}
192#graph>div:nth-child(2)>tr{
193 display: flex;
194 align-items: center;
195 grid-template-columns: repeat(auto-fit, minmax(1px, 1fr));
196}
197#graph>div:nth-child(2)>tr>td.graph_hor{
198 width: 100%;
199 height: 0;
200 padding: 0;
201
202 outline: 1px solid red;
203}
204#graph>div:nth-child(2)>tr>td.graph_ver{
205 width: 0;
206 height: 100%;
207 padding: 0;
208
209 outline: 1px solid blue;
210 transform: translateY(50%);
211 z-index: 0;
212 overflow: hidden;
213}
214
215#graph>div:nth-child(3){ /* dates */
216 padding-right: 20px;
217 z-index: 1;
218 height: 16px;
219 background-color: #202232;
220 grid-column: 1 /3;
221 font-size: 12px;
222 display: grid;
223 padding-top: 8px;
224 grid-template-columns: repeat(auto-fit, minmax(1px, 1fr));
225}
226
227.graph-button{
228 position: absolute;
229 padding: 0;
230 border: 5px solid white;
231 border-radius: 20px;
232 cursor: pointer;
233 transform: translateX(-50%);
234}
235
236#history>div>h5{text-align: center;height: 197px;}
237
238
239/* Section 4: Difficulty + count */
240
241#section4{
242 display: none;
243 grid-template-columns: 1fr 1fr;
244 gap: 20px;
245 margin: 40px 0 0 0;
246}
247
248#difficulty,
249#count {
250 background-color: #202232;
251 min-width: 250px;
252 text-align: center;
253 cursor: default;
254
255 border-radius: 24px;
256 display: grid;
257 grid-template-rows: 20px 40px 40px;
258}
259#difficulty>span:nth-child(1),
260#count>span:nth-child(1){
261 padding-top:10px;
262 font-size: 18px;
263 color:#cdcfdf
264}
265#difficulty>span:nth-child(2){
266 font-size: 40px;
267}
268#difficulty>div{
269 display: grid;
270 grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
271 padding: 0 calc(50% - 125px) 0 calc(50% - 125px);
272 place-items: center;
273}
274
275.difficulty-rating{
276 border-radius: 24px;
277 width: 40px; height: 3px;
278 background-color: #2b2e46;
279}
280
281#count>div{
282 padding-top:10px;
283 font-size: 50px;
284 color:white
285}
286
287/* Section 5: route desc + video */
288#section5{
289 margin: 40px 0 20px 0;
290 width: 100%;
291}
292
293#description{
294 width: 100%; height: auto;
295 min-height: 342px;
296}
297
298
299
300
301#description>iframe{
302 margin: 4px;
303 float:right;
304 border: 0;
305 border-radius: 24px;
306 width: 608px; height: 342px;
307}
308
309#description>h3{margin: 0 0 10px 0; color: white;}
310#description-text{
311 display: block;
312 font-size: 21px;
313 word-wrap: break-word;
314}
315#description-text>b{font-size: inherit;}
316#description-text>a{font-size: inherit;color: #3c91e6;}
317
318
319/* Section 6: leaderboards */
320#section6{
321 margin: 40px 0 20px 0;
322 min-height: 600px;
323 background-color: #202232;
324
325 border-radius: 24px;
326 padding: 10px 10px 0 10px;
327
328}
329
330
331#section6>hr{border: 1px solid #2b2e46;margin: 8px 20px 0px 20px;}
332#leaderboard-top{
333 display: grid;
334 font-size: 20px;
335 height: 34px;
336 padding-left: 60px;
337 margin: 0 20px 0 20px;
338}
339#leaderboard-top>span{
340
341 display: flex;
342 place-items: flex-end;
343}
344
345#runner{
346 display: grid;
347 grid-template-columns: 50% 50%;
348 align-items: end;
349}
350
351#page-number{
352 display: flex;
353 width: auto;
354 flex-direction: row-reverse;
355}
356#page-number>div{
357width: 100px;
358place-items: center;
359display: grid;
360grid-template-columns: 1fr 1fr 1fr;
361text-align: center;
362}
363#page-number>div>button{
364 width: 30px;
365 height: 30px;
366 background-color: #202232;
367 border: 0;
368 padding: 0;
369 cursor: pointer;
370}
371
372.leaderboard-record{
373 margin: 10px 20px 0px 20px;
374 height: 44px; width: calc(100% - 40px);
375 width: auto;
376
377 color: inherit;
378 border-radius: 40px;
379 text-align: left;
380 padding: 0 0 0 60px;
381 font-size: 20px;
382 font-family: inherit;
383
384 grid-template-columns: 3% 4.5% 40% 4% 3.5% 15% 15% 15%;
385 display: grid;
386
387 border: 0;
388 transition: background-color .1s;
389 background-color: #2b2e46;
390}
391
392.leaderboard-record>span:nth-child(1){display: grid;}
393.leaderboard-record>span:nth-child(4){display: grid;}
394.leaderboard-record>span:last-child{flex-direction: row-reverse;}
395.leaderboard-record>span{
396 display: flex;
397 place-items: center;
398 height: 44px;
399}
400
401.leaderboard-record>div>span>img{
402 height: 36px;
403 border-radius: 50px;
404 padding: 0;
405 scale: .95;
406}
407.leaderboard-record>div{
408 display: grid;
409 grid-template-columns: 50% 50%;
410 place-items: left;
411}
412.leaderboard-record>div>span{
413 display: flex;
414 place-items: center;
415 height: 44px;
416}
417
418.leaderboard-record>span>button{
419 background-color: #0000;
420 border: 0;
421 cursor: pointer;
422 transition: opacity 0.1s;
423}
424
425.hover-popup {
426 position: relative;
427 }
428
429 .hover-popup::after {
430 content: attr(popup-text);
431 position: absolute;
432 /* top: 0%; */
433 /* left: 80%; */
434 /* transform: translateX(-100%); */
435 /* padding: 5px; */
436 background-color: #2b2e46;
437 /* border: 1px solid #161723; */
438 border-radius: 8px;
439 visibility: hidden;
440 opacity: 0;
441 /* transition: visibility 0s, opacity 0.3s ease; */
442 }
443
444 .hover-popup:hover::after {
445 visibility: visible;
446 opacity: 1;
447 }
448
449.leaderboard-record:last-child{margin: 10px 20px 10px 20px;}
450
451
452#section7{
453 margin: 40px 0 20px 0;
454 background-color: #202232;
455 border-radius: 24px;
456 padding: 10px;
457}
458
459#discussion-search{
460 height: 46px;
461 width: 100%;
462 display: grid;
463 grid-template-columns: 1fr 100px;
464 margin: 0 0 20px 0;
465}
466#discussion-search>input::placeholder{color: #aaa;}
467#discussion-search>input{
468 background-color: #2b2e46;
469 font-size: 20px;
470 padding-left: 10px;
471 color: white;
472 border: 0;
473 border-radius: 16px 0 0 16px;
474 font-family: inherit;
475}
476#discussion-search>div>button:hover{filter: brightness(75%);}
477#discussion-search>div>button{
478 padding: 7px 16px;
479 margin: 8px 0;
480 border: 0;
481 font-size: 16px;
482 border-radius: 24px;
483 display: block;
484 background-color:#3c91e6;
485 font-family: inherit;
486 font-weight: bold;
487 cursor: pointer;
488 color: white;
489
490 transition: filter .2s;
491}
492#discussion-search>div{
493 background-color: #2b2e46;
494 border-radius: 0 16px 16px 0;
495}
496#discussion-post>button:nth-child(1)>span>b{font-size: 18px;color:#cdcfdf;font-weight: lighter;}
497#discussion-post>button:nth-child(1){
498 background-color: #2b2e46;
499 display: grid;
500 grid-template-columns: minmax(0, 1fr) 150px;
501
502 border-radius: 16px;
503 padding: 16px 12px;
504 margin: 8px 0 0 0;
505 border: 0;
506 width: 100%; height: 100px;
507 text-align: start;
508 white-space: nowrap;
509 color: #cdcfdf;
510 cursor: pointer;
511 overflow: hidden;
512}
513#discussion-post>button:nth-child(1)>span:nth-child(1){font-size: 32px;}
514#discussion-post>button:nth-child(1)>span:nth-child(3){color: #aaa; font-size: 18px;}
515#discussion-post>button:nth-child(1)>span:nth-child(4){
516 opacity: .7;
517 height: 40px;
518 display: flex;
519 place-items: end;
520 justify-content: end;
521}
522
523#discussion-post{height: 100px;}
524#discussion-post>button>button:hover{filter: brightness(75%); }
525#discussion-post>button>button{
526 padding: 7px 16px;
527
528 border: 0;
529 font-size: 16px;
530 border-radius: 24px;
531 background-color:#e52d04;
532 font-family: BarlowSemiCondensed-Regular;
533 font-weight: bold;
534 cursor: pointer;
535 color: white;
536
537 transition: filter .2s;
538}
539
540
541#discussion-create>div{
542 display: grid;
543 text-align: start;
544}
545#discussion-create{
546 display: grid;
547 grid-template-columns: 1fr 40px;
548 height: auto;
549 word-wrap: break-word;
550}
551
552#discussion-create>span{padding-left: 20px;}
553#discussion-create>div>input::placeholder{color: #aaa;}
554#discussion-create>div>input{
555 background-color: #2b2e46;
556 font-size: 20px;
557 padding-left: 10px;
558 margin-top: 10px;
559 height: 32px;
560 color: white;
561 border: 0;
562 font-family: inherit;
563}
564#discussion-create>div>input:nth-child(2){font-size: 16px;}
565
566#discussion-create-button:hover{filter: brightness(75%);}
567#discussion-create-button{
568 padding: 7px 16px;
569 margin: 8px 0 0 0;
570 border: 0;
571 font-size: 16px;
572 border-radius: 24px;
573
574 background-color:#3c91e6;
575 font-family: inherit;
576 font-weight: bold;
577 cursor: pointer;
578 color: white;
579 width: min-content;
580 grid-column: 1 / span 2;
581
582
583 transition: filter .2s;
584}
585
586
587#discussion-thread>div:nth-child(1){
588 display: grid;
589 grid-template-columns: 1fr 40px;
590 height: auto;
591 padding: 0 0 10px 20px;
592 word-wrap: break-word;
593}
594
595#discussion-create>button:nth-child(2),
596#discussion-thread>div>button{
597 height: 40px;
598 float:inline-end;
599 color:#cdcfdf;
600 background-color: #0000;
601 border: 0;
602 font-size: 38px;
603 cursor: pointer;
604}
605
606
607#discussion-thread>div:nth-child(2)>img{
608 width: 60px; height: 60px;
609 border-radius: 100px;
610 margin: 20px 0 0 0;
611}
612#discussion-thread>div:nth-child(2)>div{
613 height: max-content;
614 padding: 20px 0 0 10px;
615 display: inline-grid;
616 grid-template-columns: min-content 1fr ;
617 overflow: hidden;
618
619}
620#discussion-thread>div:nth-child(2)>div>span:nth-child(1){font-weight: bold;height: 30px;}
621#discussion-thread>div:nth-child(2)>div>span:nth-child(2){
622 opacity: 0.6;
623 height: 30px;
624 font-size: 80%;
625 padding-left: 10px;
626}
627#discussion-thread>div:nth-child(2)>div>span:nth-child(3){
628 grid-column: 1 / span 2;
629 height: max-content;
630 word-wrap: break-word;
631}
632#discussion-thread>div:nth-child(2){
633 display: grid;
634 grid-template-columns: 60px 1fr;
635 font-size: 20px;
636 max-height: 522px;
637 overflow-y: auto;
638}
639
640
641#discussion-send{
642 height: 48px;
643 width: 100%;
644 display: grid;
645 grid-template-columns: 1fr 80px;
646 margin: 10px 0 0 0;
647}
648#discussion-send>input::placeholder{color: #aaa;}
649#discussion-send>input{
650 background-color: #2b2e46;
651 padding-left: 10px;
652 color: white;
653 border: 0;
654 font-size: 20px;
655 border-radius: 16px 0 0 16px;
656 font-family: inherit;
657}
658#discussion-send>div{
659 background-color: #2b2e46;
660 border-radius: 0 16px 16px 0;
661
662}
663#discussion-send>div>button:hover{ filter: brightness(75%);}
664#discussion-send>div>button{
665 padding: 7px 20px;
666 margin: 8px 0;
667 font-size: 16px;
668 border: 0;
669 border-radius: 24px;
670 display: block;
671 background-color:#3c91e6;
672 font-family: inherit;
673 font-weight: bold;
674 cursor: pointer;
675 color: white;
676
677 transition: filter .2s;
678}
679
680
681
682.triangle{
683 display: inline-block;
684 width: 8px; height: 0;
685 border-top: 7px solid transparent;
686 border-right: 8px solid #cdcfdf;
687 border-bottom: 7px solid transparent;
688}
689
690 /* such responsive, very mobile */
691@media screen and (max-width: 1480px) {
692 #section3.summary1{grid-template-columns: auto;}
693 #category{min-width: 608px;}
694 #history{min-width: 608px;}
695 #description{min-width: 608px;}
696 #section4.summary1{min-width: 588px;}
697
698 #description>iframe{
699 padding: 0 0 0 calc(50% - 304px);
700 float:none;
701 justify-content: center;
702 align-items: center;
703 }
704
705 #section1.summary1{
706 grid-template-columns: auto;
707 place-items: center;
708 text-align: center;
709
710 }
711
712 #section2.summary1{
713 grid-template-columns: auto;
714 width: 450px;
715 margin: 40px auto 0 auto;
716 }
717 #section2.summary1>.nav-button:nth-child(1){border-radius: 30px 30px 0 0;}
718 #section2.summary1>.nav-button:nth-child(2){border-radius: 0;}
719 #section2.summary1>.nav-button:nth-child(3){border-radius: 0 0 30px 30px;}
720} \ No newline at end of file
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.css b/frontend/src/components/pages/summary_modview.css
deleted file mode 100644
index c6d3d8d..0000000
--- a/frontend/src/components/pages/summary_modview.css
+++ /dev/null
@@ -1,112 +0,0 @@
1div#modview{
2 position: absolute;
3 left: 50%;
4 z-index: 20;
5 width: 320px; height: auto;
6 /* background-color: red; */
7
8 transform: translateY(-68%);
9}
10div#modview>div>button{
11 height: 30px;
12}
13
14div#modview>div:nth-child(1){
15 display: grid;
16 grid-template-columns: 50% 50%;
17}
18
19div#modview>div:nth-child(2){
20 display: grid;
21 place-items: center;
22}
23
24#modview-menu{
25 position: absolute;
26 left: calc(50% + 160px); top: 130px;
27 transform: translateX(-50%);
28 background-color: #2b2e46;
29 z-index: 2; color: white;
30}
31
32#modview-menu-image{
33 box-shadow: 0 0 40px 16px black;
34 outline: 8px solid #2b2e46;
35 border-radius: 20px;
36 font-size: 40px;
37 display: grid;
38 grid-template-columns: 50% 50%;
39 /* place-items: center; */
40
41}
42#modview-menu-image>div:nth-child(1){
43 height: 400px; width: 500px;
44 display: grid;
45 grid-template-rows: 30% 70%;
46}
47#modview-menu-image>div:nth-child(2){
48 height: 400px; width: 500px;
49 display: grid;
50 grid-template-rows: 20% 10%;
51}
52
53#modview-menu-image>div>button{width: 300px;margin-left:100px;}
54#modview-menu-image>div>img{width: 500px;}
55#modview-menu-image>div>button{font-size: 20px;}
56#modview-menu-image>div>span>input[type="file"]{font-size: 15px;}
57
58
59#modview-menu-add,
60#modview-menu-edit{
61 box-shadow: 0 0 40px 16px black;
62 outline: 8px solid #2b2e46;
63 border-radius: 20px;
64 font-size: 40px;
65 display: grid;
66 grid-template-columns: 20% 20% 20% 20% 20%;
67}
68
69#modview-menu-add>div,
70#modview-menu-edit>div{
71 display: grid;
72 margin: 20px;
73 width: 200px;
74 font-size: 20px;
75}
76#modview-route-description>textarea{
77 resize: none;
78 height: 160px;
79 width: 1160px;
80}
81#modview-route-showcase>input::placeholder{opacity: .5;}
82#modview_block{
83 position: absolute;
84 background-color: black;
85 opacity: .3;
86 left: 320px;
87 width: calc(100% - 320px);
88 height: 100%;
89 z-index: 2;
90 cursor: no-drop;
91}
92#modview-md{
93 box-shadow: 0 0 40px 16px black;
94 background-color: #2b2e46;
95 outline: 8px solid #2b2e46;
96
97 border-radius: 20px;
98 position: absolute;
99 padding: 10px; top: 400px;
100 width: 1180px; height: 300px;
101 overflow-y: auto;
102 word-wrap: break-word;
103}
104#modview-md>span>a{
105 padding-left: 20px;
106 color:aqua;
107}
108#modview-md>p{
109 font-family: BarlowSemiCondensed-Regular;
110 color: #cdcfdf;
111 font-size: 21px;
112} \ No newline at end of file
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.css b/frontend/src/components/sidebar.css
deleted file mode 100644
index 34ede80..0000000
--- a/frontend/src/components/sidebar.css
+++ /dev/null
@@ -1,208 +0,0 @@
1#sidebar {
2 overflow: hidden;
3 position: absolute;
4 background-color: #2b2e46;
5 width: 320px; height: 100vh;
6 min-height: 670px;
7
8}
9
10 /* logo */
11#logo{
12 display: grid;
13 grid-template-columns: 60px 200px;
14
15
16 height: 80px;
17 padding: 20px 0 20px 30px;
18 cursor: pointer;
19 user-select: none;
20}
21
22#logo-text{
23 font-family: BarlowCondensed-Regular;
24 font-size: 42px;
25 color: #FFF;
26 line-height: 38px;
27}
28span>b{
29 font-family: BarlowCondensed-Bold;
30 font-size: 56px;
31}
32
33 /* Sidelist */
34#sidebar-list{
35 z-index: 2;
36 background-color: #2b2e46;
37 position: relative;
38 height: calc(100vh - 120px);
39 width: 320px;
40 /* min-height: 670px; */
41 transition: width .3s;
42}
43#sidebar-toplist>button:nth-child(1){margin-top: 5px;}
44#sidebar-toplist{
45 display: grid;
46
47 margin: 0 5px 0 5px;
48 justify-items: left;
49 height: 400px;
50 grid-template-rows: 45px 50px 50px 50px 50px 50px 50px 50px auto;
51}
52
53#sidebar-bottomlist{
54 display: grid;
55
56 margin: 0 5px 0 5px;
57 justify-items: left;
58 grid-template-rows: calc(100vh - 670px) 50px 50px 50px;
59}
60.sidebar-button>span{
61 font-family: BarlowSemiCondensed-Regular;
62 font-size: 18px;
63 color: #CDCFDF;
64 height: 32px;
65 line-height: 28px;
66 transition: opacity .1s;
67}
68.sidebar-button{
69 display: grid;
70 grid-template-columns: 50px auto;
71 place-items: left;
72 text-align: left;
73
74 background-color: inherit;
75 cursor: pointer;
76 border: none;
77 width: 310px;
78 height: 40px;
79 border-radius: 20px;
80 padding: 0.4em 0 0 11px;
81
82 transition:
83 width .3s,
84 background-color .15s,
85 padding .3s;
86}
87
88.sidebar-button-selected {
89 background-color: #202232;
90}
91
92.sidebar-button-deselected {
93 background-color: #20223200;
94}
95
96.sidebar-button-deselected:hover {
97 background-color: #202232aa;
98}
99
100button>img {
101 scale: 1.1;
102 width: 20px;
103 padding: 5px;
104}
105
106 /* Maplist */
107#sidebar>div:nth-child(3){
108 position: relative;
109 background-color: #202232;
110 color: #424562;
111 z-index: 1;
112
113 left: 52px;
114 top: calc(-100vh + 120px);
115 width: 268px; height: calc(100vh - 120px);
116 min-height: 550px;
117}
118input#searchbar[type=text]{
119 margin: 10px 0 0 6px;
120 padding: 1px 0px 1px 16px;
121 width: 240px;
122 height: 30px;
123
124 font-family: BarlowSemiCondensed-Regular;
125 font-size: 20px;
126
127 background-color: #161723;
128 color:#CDCFDF;
129
130 border: 0;
131 border-radius: 20px;
132
133}
134input[type=text]::placeholder{color:#2b2e46}
135input[type=text]:focus{outline: inherit;}
136a{text-decoration: none;height: 40px;}
137
138
139#search-data{
140 margin: 8px 0 8px 0;
141 overflow-y: auto;
142 max-height: calc(100vh - 172px);
143 scrollbar-width: thin;
144}
145#search-data::-webkit-scrollbar{display: none;}
146.search-map{
147 margin: 10px 6px 0 6px;
148 height: 80px;
149
150 border-radius: 20px;
151 text-align: center;
152
153 display: grid;
154
155 border: 0;
156 transition: background-color .1s;
157 background-color: #2b2e46;
158 grid-template-rows: 20% 20% 60%;
159 width: calc(100% - 15px);
160}
161.search-map>span{
162 color: #888;
163 font-size: 16px;
164 font-family: BarlowSemiCondensed-Regular;
165}
166.search-map>span:nth-child(3){
167 font-size: 30px;
168 color: #CDCFDF;
169}
170
171.search-player{
172 overflow: hidden;
173 margin: 10px 6px 0 6px;
174 height: 80px;
175
176 border-radius: 20px;
177 text-align: center;
178 color: #CDCFDF;
179 font-family: BarlowSemiCondensed-Regular;
180
181 display: grid;
182 place-items: center;
183 grid-template-columns: 20% 80%;
184 padding: 0 16px 0 16px;
185
186 border: 0;
187 transition: background-color .1s;
188 background-color: #2b2e46;
189}
190.search-player>img{
191 height: 60px;
192 border-radius: 20px;
193}
194.search-player>span{
195 width:154px;
196 font-size: 26px;
197}
198
199
200
201
202
203
204
205
206
207
208
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