aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArda Serdar Pektezol <1669855+pektezol@users.noreply.github.com>2024-10-09 16:34:12 +0300
committerArda Serdar Pektezol <1669855+pektezol@users.noreply.github.com>2024-10-09 16:34:12 +0300
commita7c282ca348c1e8e60559e5c064caee28ba11eec (patch)
tree43bd7bdb2bbc80b92b96a14b36c33f0b7df622c9
parentRankings page (diff)
downloadlphub-a7c282ca348c1e8e60559e5c064caee28ba11eec.tar.gz
lphub-a7c282ca348c1e8e60559e5c064caee28ba11eec.tar.bz2
lphub-a7c282ca348c1e8e60559e5c064caee28ba11eec.zip
refactor: so much shit
-rw-r--r--backend/go.mod1
-rw-r--r--backend/go.sum2
-rw-r--r--backend/handlers/login.go2
-rw-r--r--backend/main.go18
-rw-r--r--frontend/src/App.tsx46
-rw-r--r--frontend/src/api/Api.tsx205
-rw-r--r--frontend/src/api/Auth.tsx14
-rw-r--r--frontend/src/api/Games.tsx31
-rw-r--r--frontend/src/api/Maps.tsx76
-rw-r--r--frontend/src/api/Mod.tsx58
-rw-r--r--frontend/src/api/Rankings.tsx13
-rw-r--r--frontend/src/api/User.tsx25
-rw-r--r--frontend/src/components/Discussions.tsx45
-rw-r--r--frontend/src/components/Leaderboards.tsx6
-rw-r--r--frontend/src/components/Login.tsx1923
-rw-r--r--frontend/src/components/ModMenu.tsx44
-rw-r--r--frontend/src/components/RankingEntry.tsx2
-rw-r--r--frontend/src/components/Sidebar.tsx2
-rw-r--r--frontend/src/components/UploadRunDialog.tsx6
-rw-r--r--frontend/src/css/Profile.css6
-rw-r--r--frontend/src/pages/Homepage.tsx2
-rw-r--r--frontend/src/pages/Maplist.tsx5
-rw-r--r--frontend/src/pages/Maps.tsx14
-rw-r--r--frontend/src/pages/Profile.tsx131
-rw-r--r--frontend/src/pages/Rankings.tsx65
-rw-r--r--frontend/src/pages/Rules.tsx4
-rw-r--r--frontend/src/pages/User.tsx146
-rw-r--r--frontend/src/utils/Jwt.tsx44
-rw-r--r--rankings/input/records.json8
-rw-r--r--rankings/main.go25
30 files changed, 635 insertions, 2334 deletions
diff --git a/backend/go.mod b/backend/go.mod
index f6eef48..ae50685 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -8,6 +8,7 @@ require (
8) 8)
9 9
10require ( 10require (
11 github.com/gin-contrib/cors v1.7.2
11 github.com/golang-jwt/jwt/v4 v4.5.0 12 github.com/golang-jwt/jwt/v4 v4.5.0
12 github.com/google/uuid v1.6.0 13 github.com/google/uuid v1.6.0
13 github.com/pektezol/steam_go v1.1.2 14 github.com/pektezol/steam_go v1.1.2
diff --git a/backend/go.sum b/backend/go.sum
index 10504e4..f117b31 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -31,6 +31,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
31github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 31github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
32github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= 32github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
33github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= 33github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
34github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
35github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
34github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= 36github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
35github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= 37github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
36github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 38github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
diff --git a/backend/handlers/login.go b/backend/handlers/login.go
index 565ebdb..408d950 100644
--- a/backend/handlers/login.go
+++ b/backend/handlers/login.go
@@ -4,6 +4,7 @@ import (
4 "encoding/json" 4 "encoding/json"
5 "fmt" 5 "fmt"
6 "io" 6 "io"
7 "log"
7 "net/http" 8 "net/http"
8 "os" 9 "os"
9 "time" 10 "time"
@@ -29,6 +30,7 @@ type LoginResponse struct {
29// @Success 200 {object} models.Response{data=LoginResponse} 30// @Success 200 {object} models.Response{data=LoginResponse}
30// @Router /login [get] 31// @Router /login [get]
31func Login(c *gin.Context) { 32func Login(c *gin.Context) {
33 log.Println(c.Request.Host)
32 openID := steam_go.NewOpenID(c.Request, true) 34 openID := steam_go.NewOpenID(c.Request, true)
33 switch openID.Mode() { 35 switch openID.Mode() {
34 case "": 36 case "":
diff --git a/backend/main.go b/backend/main.go
index 202c607..306823c 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -4,11 +4,13 @@ import (
4 "fmt" 4 "fmt"
5 "log" 5 "log"
6 "os" 6 "os"
7 "time"
7 8
8 "lphub/api" 9 "lphub/api"
9 "lphub/database" 10 "lphub/database"
10 _ "lphub/docs" 11 _ "lphub/docs"
11 12
13 "github.com/gin-contrib/cors"
12 "github.com/gin-gonic/gin" 14 "github.com/gin-gonic/gin"
13 "github.com/joho/godotenv" 15 "github.com/joho/godotenv"
14) 16)
@@ -31,7 +33,23 @@ func main() {
31 gin.SetMode(gin.ReleaseMode) 33 gin.SetMode(gin.ReleaseMode)
32 } 34 }
33 router := gin.Default() 35 router := gin.Default()
36 router.Use(cors.New(cors.Config{
37 AllowOrigins: []string{"*"},
38 AllowMethods: []string{"GET", "POST", "DELETE", "PUT", "PATCH"},
39 AllowHeaders: []string{"Origin"},
40 ExposeHeaders: []string{"Content-Length"},
41 AllowCredentials: true,
42 AllowOriginFunc: func(origin string) bool {
43 return origin == "https://github.com"
44 },
45 MaxAge: 12 * time.Hour,
46 }))
34 database.ConnectDB() 47 database.ConnectDB()
35 api.InitRoutes(router) 48 api.InitRoutes(router)
49 router.Static("/static", "../frontend/build/static")
50 router.StaticFile("/", "../frontend/build/index.html")
51 router.NoRoute(func(c *gin.Context) {
52 c.File("../frontend/build/index.html")
53 })
36 router.Run(fmt.Sprintf(":%s", os.Getenv("PORT"))) 54 router.Run(fmt.Sprintf(":%s", os.Getenv("PORT")))
37} 55}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 3980e1b..3a7fa18 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,5 @@
1import React from 'react'; 1import React from 'react';
2import { Routes, Route } from "react-router-dom"; 2import { Routes, Route, useLocation } from "react-router-dom";
3 3
4import { UserProfile } from './types/Profile'; 4import { UserProfile } from './types/Profile';
5import Sidebar from './components/Sidebar'; 5import Sidebar from './components/Sidebar';
@@ -17,29 +17,53 @@ import { Game } from './types/Game';
17import { API } from './api/Api'; 17import { API } from './api/Api';
18import Maplist from './pages/Maplist'; 18import Maplist from './pages/Maplist';
19import Rankings from './pages/Rankings'; 19import Rankings from './pages/Rankings';
20import { get_user_id_from_token, get_user_mod_from_token } from './utils/Jwt';
20 21
21const App: React.FC = () => { 22const App: React.FC = () => {
22 const [token, setToken] = React.useState<string | undefined>(undefined); 23 const [token, setToken] = React.useState<string | undefined>(undefined);
23 const [profile, setProfile] = React.useState<UserProfile | undefined>(undefined); 24 const [profile, setProfile] = React.useState<UserProfile | undefined>(undefined);
24 const [isModerator, setIsModerator] = React.useState<boolean>(true); 25 const [isModerator, setIsModerator] = React.useState<boolean>(false);
25 26
26 const [games, setGames] = React.useState<Game[]>([]); 27 const [games, setGames] = React.useState<Game[]>([]);
27 28
28 const [uploadRunDialog, setUploadRunDialog] = React.useState<boolean>(false); 29 const [uploadRunDialog, setUploadRunDialog] = React.useState<boolean>(false);
29 const [uploadRunDialogMapID, setUploadRunDialogMapID] = React.useState<number | undefined>(undefined); 30 const [uploadRunDialogMapID, setUploadRunDialogMapID] = React.useState<number | undefined>(undefined);
30 31
31 // React.useEffect(() => { 32 const _fetch_token = async () => {
32 // if (token) { 33 const token = await API.get_token();
33 // setIsModerator(JSON.parse(atob(token.split(".")[1])).mod) 34 setToken(token);
34 // } 35 };
35 // }, [token]);
36 36
37 const _fetch_games = async () => { 37 const _fetch_games = async () => {
38 const games = await API.get_games(); 38 const games = await API.get_games();
39 setGames(games); 39 setGames(games);
40 }; 40 };
41 41
42 const _set_profile = async (user_id: string | undefined) => {
43 if (user_id) {
44 setProfile({} as UserProfile); // placeholder before we call actual user profile
45 const user = await API.get_profile(token!);
46 setProfile(user);
47 }
48 };
49
50 React.useEffect(() => {
51 if (token === undefined) {
52 setProfile(undefined);
53 setIsModerator(false);
54 } else {
55 _set_profile(get_user_id_from_token(token))
56 const modStatus = get_user_mod_from_token(token)
57 if (modStatus) {
58 setIsModerator(true);
59 } else {
60 setIsModerator(false);
61 }
62 }
63 }, [token]);
64
42 React.useEffect(() => { 65 React.useEffect(() => {
66 _fetch_token();
43 _fetch_games(); 67 _fetch_games();
44 }, []); 68 }, []);
45 69
@@ -55,14 +79,14 @@ const App: React.FC = () => {
55 <Sidebar setToken={setToken} profile={profile} setProfile={setProfile} onUploadRun={() => setUploadRunDialog(true)} /> 79 <Sidebar setToken={setToken} profile={profile} setProfile={setProfile} onUploadRun={() => setUploadRunDialog(true)} />
56 <Routes> 80 <Routes>
57 <Route path="/" element={<Homepage />} /> 81 <Route path="/" element={<Homepage />} />
58 <Route path="/profile" element={<Profile profile={profile} />} /> 82 <Route path="/profile" element={<Profile profile={profile} token={token} gameData={games} />} />
59 <Route path="/users/*" element={<User />} /> 83 <Route path="/users/*" element={<User profile={profile} token={token} gameData={games} />} />
60 <Route path="/games" element={<Games games={games} />} /> 84 <Route path="/games" element={<Games games={games} />} />
61 <Route path='/games/:id' element={<Maplist />}></Route> 85 <Route path='/games/:id' element={<Maplist />}></Route>
62 <Route path="/maps/*" element={<Maps profile={profile} isModerator={isModerator} onUploadRun={(mapID) => {setUploadRunDialog(true);setUploadRunDialogMapID(mapID)}} />}/> 86 <Route path="/maps/*" element={<Maps token={token} isModerator={isModerator} />}/>
63 <Route path="/rules" element={<Rules />} /> 87 <Route path="/rules" element={<Rules />} />
64 <Route path="/about" element={<About />} /> 88 <Route path="/about" element={<About />} />
65 <Route path='/rankings' element={<Rankings></Rankings>}></Route> 89 <Route path='/rankings' element={<Rankings />}></Route>
66 <Route path="*" element={"404"} /> 90 <Route path="*" element={"404"} />
67 </Routes> 91 </Routes>
68 </> 92 </>
diff --git a/frontend/src/api/Api.tsx b/frontend/src/api/Api.tsx
index 30c0ad6..6731cb3 100644
--- a/frontend/src/api/Api.tsx
+++ b/frontend/src/api/Api.tsx
@@ -1,197 +1,52 @@
1import axios from 'axios';
2
3import { GameChapter, GamesChapters } from '../types/Chapters';
4import { Game } from '../types/Game';
5import { Map, MapDiscussion, MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map';
6import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '../types/Content'; 1import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '../types/Content';
7import { Search } from '../types/Search'; 2import { delete_token, get_token } from './Auth';
8import { UserProfile } from '../types/Profile'; 3import { get_user, get_profile, post_profile } from './User';
9import { Ranking } from '../types/Ranking'; 4import { get_games, get_chapters, get_games_chapters, get_game_maps, get_search } from './Games';
5import { get_official_rankings, get_unofficial_rankings } from './Rankings';
6import { get_map_summary, get_map_leaderboard, get_map_discussions, get_map_discussion, post_map_discussion, post_map_discussion_comment, delete_map_discussion } from './Maps';
7import { delete_map_summary, post_map_summary, put_map_image, put_map_summary } from './Mod';
10 8
11// add new api call function entries here 9// add new api call function entries here
12// example usage: API.get_games(); 10// example usage: API.get_games();
13export const API = { 11export const API = {
14 user_logout: () => user_logout(), 12 // Auth
15 13 get_token: () => get_token(),
14
15 delete_token: () => delete_token(),
16 // User
16 get_user: (user_id: string) => get_user(user_id), 17 get_user: (user_id: string) => get_user(user_id),
18 get_profile: (token: string) => get_profile(token),
19 post_profile: (token: string) => post_profile(token),
20 // Games
17 get_games: () => get_games(), 21 get_games: () => get_games(),
18
19 get_chapters: (chapter_id: string) => get_chapters(chapter_id), 22 get_chapters: (chapter_id: string) => get_chapters(chapter_id),
20 get_games_chapters: (game_id: string) => get_games_chapters(game_id), 23 get_games_chapters: (game_id: string) => get_games_chapters(game_id),
21 get_game_maps: (game_id: string) => get_game_maps(game_id), 24 get_game_maps: (game_id: string) => get_game_maps(game_id),
22 get_rankings: () => get_rankings(),
23 get_search: (q: string) => get_search(q), 25 get_search: (q: string) => get_search(q),
24 26 // Rankings
27 get_official_rankings: () => get_official_rankings(),
28 get_unofficial_rankings: () => get_unofficial_rankings(),
29 // Maps
25 get_map_summary: (map_id: string) => get_map_summary(map_id), 30 get_map_summary: (map_id: string) => get_map_summary(map_id),
26 get_map_leaderboard: (map_id: string) => get_map_leaderboard(map_id), 31 get_map_leaderboard: (map_id: string) => get_map_leaderboard(map_id),
27 get_map_discussions: (map_id: string) => get_map_discussions(map_id), 32 get_map_discussions: (map_id: string) => get_map_discussions(map_id),
28 get_map_discussion: (map_id: string, discussion_id: number) => get_map_discussion(map_id, discussion_id), 33 get_map_discussion: (map_id: string, discussion_id: number) => get_map_discussion(map_id, discussion_id),
29 34
30 post_map_summary: (map_id: string, content: ModMenuContent) => post_map_summary(map_id, content), 35 post_map_discussion: (token: string, map_id: string, content: MapDiscussionContent) => post_map_discussion(token, map_id, content),
31 post_map_discussion: (map_id: string, content: MapDiscussionContent) => post_map_discussion(map_id, content), 36 post_map_discussion_comment: (token: string, map_id: string, discussion_id: number, comment: string) => post_map_discussion_comment(token, map_id, discussion_id, comment),
32 post_map_discussion_comment: (map_id: string, discussion_id: number, content: MapDiscussionCommentContent) => post_map_discussion_comment(map_id, discussion_id, content),
33
34 put_map_image: (map_id: string, image: string) => put_map_image(map_id, image),
35 put_map_summary: (map_id: string, content: ModMenuContent) => put_map_summary(map_id, content),
36 37
37 delete_map_summary: (map_id: string, route_id: number) => delete_map_summary(map_id, route_id), 38 delete_map_discussion: (token: string, map_id: string, discussion_id: number) => delete_map_discussion(token, map_id, discussion_id),
38 delete_map_discussion: (map_id: string, discussion_id: number) => delete_map_discussion(map_id, discussion_id), 39 // Mod
40 post_map_summary: (token: string, map_id: string, content: ModMenuContent) => post_map_summary(token, map_id, content),
41
42 put_map_image: (token: string, map_id: string, image: string) => put_map_image(token, map_id, image),
43 put_map_summary: (token: string, map_id: string, content: ModMenuContent) => put_map_summary(token, map_id, content),
44
45 delete_map_summary: (token: string, map_id: string, route_id: number) => delete_map_summary(token, map_id, route_id),
39}; 46};
40 47
41const BASE_API_URL: string = "https://lp.ardapektezol.com/api/v1/" 48const BASE_API_URL: string = "/api/v1/"
42 49
43function url(path: string): string { 50export function url(path: string): string {
44 return BASE_API_URL + path; 51 return BASE_API_URL + path;
45}
46
47// USER
48
49const user_logout = async () => {
50 await axios.delete(url("token"));
51};
52
53const get_user = async (user_id: string): Promise<UserProfile> => {
54 const response = await axios.get(url(`users/${user_id}`))
55 return response.data.data;
56};
57
58
59// GAMES
60
61const get_games = async (): Promise<Game[]> => {
62 const response = await axios.get(url("games"))
63 return response.data.data;
64};
65
66const get_chapters = async (chapter_id: string): Promise<GameChapter> => {
67 const response = await axios.get(url(`chapters/${chapter_id}`));
68 return response.data.data;
69}
70
71const get_games_chapters = async (game_id: string): Promise<GamesChapters> => {
72 const response = await axios.get(url(`games/${game_id}`));
73 return response.data.data;
74};
75
76const get_game_maps = async (game_id: string): Promise<Map[]> => {
77 const response = await axios.get(url(`games/${game_id}/maps`))
78 return response.data.data.maps;
79};
80
81
82// RANKINGS
83const get_rankings = async (): Promise<Ranking> => {
84 const response = await axios.get(url(`rankings`));
85 return response.data.data;
86}
87
88// SEARCH
89
90const get_search = async (q: string): Promise<Search> => {
91 const response = await axios.get(url(`search?q=${q}`))
92 return response.data.data;
93};
94
95// MAP SUMMARY
96
97const put_map_image = async (map_id: string, image: string): Promise<boolean> => {
98 const response = await axios.put(url(`maps/${map_id}/image`), {
99 "image": image,
100 });
101 return response.data.success;
102};
103
104const get_map_summary = async (map_id: string): Promise<MapSummary> => {
105 const response = await axios.get(url(`maps/${map_id}/summary`))
106 return response.data.data;
107};
108
109const post_map_summary = async (map_id: string, content: ModMenuContent): Promise<boolean> => {
110 const response = await axios.post(url(`maps/${map_id}/summary`), {
111 "user_name": content.name,
112 "score_count": content.score,
113 "record_date": content.date,
114 "showcase": content.showcase,
115 "description": content.description,
116 });
117 return response.data.success;
118};
119
120const put_map_summary = async (map_id: string, content: ModMenuContent): Promise<boolean> => {
121 const response = await axios.put(url(`maps/${map_id}/summary`), {
122 "route_id": content.id,
123 "user_name": content.name,
124 "score_count": content.score,
125 "record_date": content.date,
126 "showcase": content.showcase,
127 "description": content.description,
128 });
129 return response.data.success;
130};
131
132const delete_map_summary = async (map_id: string, route_id: number): Promise<boolean> => {
133 const response = await axios.delete(url(`maps/${map_id}/summary`), {
134 data: {
135 "route_id": route_id,
136 }
137 });
138 return response.data.success;
139};
140
141// MAP LEADERBOARDS
142
143const get_map_leaderboard = async (map_id: string): Promise<MapLeaderboard | undefined> => {
144 const response = await axios.get(url(`maps/${map_id}/leaderboards`))
145 if (!response.data.success) {
146 return undefined;
147 }
148 const data = response.data.data;
149 // map the kind of leaderboard
150 data.records = data.records.map((record: any) => {
151 if (record.host && record.partner) {
152 return { ...record, kind: 'multiplayer' };
153 } else {
154 return { ...record, kind: 'singleplayer' };
155 }
156 });
157 // should be unreachable
158 return undefined;
159};
160
161// MAP DISCUSSIONS
162
163const get_map_discussions = async (map_id: string): Promise<MapDiscussions | undefined> => {
164 const response = await axios.get(url(`maps/${map_id}/discussions`));
165 if (!response.data.data.discussions) {
166 return undefined;
167 }
168 return response.data.data;
169};
170
171const get_map_discussion = async (map_id: string, discussion_id: number): Promise<MapDiscussion | undefined> => {
172 const response = await axios.get(url(`maps/${map_id}/discussions/${discussion_id}`));
173 if (!response.data.data.discussion) {
174 return undefined;
175 }
176 return response.data.data;
177};
178
179const post_map_discussion = async (map_id: string, content: MapDiscussionContent): Promise<boolean> => {
180 const response = await axios.post(url(`maps/${map_id}/discussions`), {
181 "title": content.title,
182 "content": content.content,
183 });
184 return response.data.success;
185};
186
187const post_map_discussion_comment = async (map_id: string, discussion_id: number, content: MapDiscussionCommentContent): Promise<boolean> => {
188 const response = await axios.post(url(`maps/${map_id}/discussions/${discussion_id}`), {
189 "comment": content.comment,
190 });
191 return response.data.success;
192};
193
194const delete_map_discussion = async (map_id: string, discussion_id: number): Promise<boolean> => {
195 const response = await axios.delete(url(`maps/${map_id}/discussions/${discussion_id}`));
196 return response.data.success;
197}; 52};
diff --git a/frontend/src/api/Auth.tsx b/frontend/src/api/Auth.tsx
new file mode 100644
index 0000000..09269e6
--- /dev/null
+++ b/frontend/src/api/Auth.tsx
@@ -0,0 +1,14 @@
1import axios from "axios";
2import { url } from "./Api";
3
4export const get_token = async (): Promise<string | undefined> => {
5 const response = await axios.get(url(`token`))
6 if (!response.data.success) {
7 return undefined;
8 }
9 return response.data.data.token;
10};
11
12export const delete_token = async () => {
13 await axios.delete(url("token"));
14};
diff --git a/frontend/src/api/Games.tsx b/frontend/src/api/Games.tsx
new file mode 100644
index 0000000..84b5f74
--- /dev/null
+++ b/frontend/src/api/Games.tsx
@@ -0,0 +1,31 @@
1import axios from "axios";
2import { url } from "./Api";
3import { GameChapter, GamesChapters } from "../types/Chapters";
4import { Game } from "../types/Game";
5import { Map } from "../types/Map";
6import { Search } from "../types/Search";
7
8export const get_games = async (): Promise<Game[]> => {
9 const response = await axios.get(url(`games`))
10 return response.data.data;
11};
12
13export const get_chapters = async (chapter_id: string): Promise<GameChapter> => {
14 const response = await axios.get(url(`chapters/${chapter_id}`));
15 return response.data.data;
16}
17
18export const get_games_chapters = async (game_id: string): Promise<GamesChapters> => {
19 const response = await axios.get(url(`games/${game_id}`));
20 return response.data.data;
21};
22
23export const get_game_maps = async (game_id: string): Promise<Map[]> => {
24 const response = await axios.get(url(`games/${game_id}/maps`))
25 return response.data.data.maps;
26};
27
28export const get_search = async (q: string): Promise<Search> => {
29 const response = await axios.get(url(`search?q=${q}`))
30 return response.data.data;
31};
diff --git a/frontend/src/api/Maps.tsx b/frontend/src/api/Maps.tsx
new file mode 100644
index 0000000..fbad78c
--- /dev/null
+++ b/frontend/src/api/Maps.tsx
@@ -0,0 +1,76 @@
1import axios from "axios";
2import { url } from "./Api";
3import { MapDiscussionContent } from "../types/Content";
4import { MapSummary, MapLeaderboard, MapDiscussions, MapDiscussion } from "../types/Map";
5
6export const get_map_summary = async (map_id: string): Promise<MapSummary> => {
7 const response = await axios.get(url(`maps/${map_id}/summary`))
8 return response.data.data;
9};
10
11export const get_map_leaderboard = async (map_id: string): Promise<MapLeaderboard | undefined> => {
12 const response = await axios.get(url(`maps/${map_id}/leaderboards`));
13 console.log(response)
14 if (!response.data.success) {
15 return undefined;
16 }
17 const data = response.data.data;
18 console.log(data.records)
19 // map the kind of leaderboard
20 data.records = data.records.map((record: any) => {
21 if (record.host && record.partner) {
22 return { ...record, kind: 'multiplayer' };
23 } else {
24 return { ...record, kind: 'singleplayer' };
25 }
26 });
27 return data;
28};
29
30export const get_map_discussions = async (map_id: string): Promise<MapDiscussions | undefined> => {
31 const response = await axios.get(url(`maps/${map_id}/discussions`));
32 if (!response.data.data.discussions) {
33 return undefined;
34 }
35 return response.data.data;
36};
37
38export const get_map_discussion = async (map_id: string, discussion_id: number): Promise<MapDiscussion | undefined> => {
39 const response = await axios.get(url(`maps/${map_id}/discussions/${discussion_id}`));
40 if (!response.data.data.discussion) {
41 return undefined;
42 }
43 return response.data.data;
44};
45
46export const post_map_discussion = async (token: string, map_id: string, content: MapDiscussionContent): Promise<boolean> => {
47 const response = await axios.post(url(`maps/${map_id}/discussions`), {
48 "title": content.title,
49 "content": content.content,
50 }, {
51 headers: {
52 "Authorization": token,
53 }
54 });
55 return response.data.success;
56};
57
58export const post_map_discussion_comment = async (token: string, map_id: string, discussion_id: number, comment: string): Promise<boolean> => {
59 const response = await axios.post(url(`maps/${map_id}/discussions/${discussion_id}`), {
60 "comment": comment,
61 }, {
62 headers: {
63 "Authorization": token,
64 }
65 });
66 return response.data.success;
67};
68
69export const delete_map_discussion = async (token: string, map_id: string, discussion_id: number): Promise<boolean> => {
70 const response = await axios.delete(url(`maps/${map_id}/discussions/${discussion_id}`), {
71 headers: {
72 "Authorization": token,
73 }
74 });
75 return response.data.success;
76};
diff --git a/frontend/src/api/Mod.tsx b/frontend/src/api/Mod.tsx
new file mode 100644
index 0000000..9091379
--- /dev/null
+++ b/frontend/src/api/Mod.tsx
@@ -0,0 +1,58 @@
1import axios from "axios";
2import { url } from "./Api";
3import { ModMenuContent } from "../types/Content";
4
5export const put_map_image = async (token: string, map_id: string, image: string): Promise<boolean> => {
6 const response = await axios.put(url(`maps/${map_id}/image`), {
7 "image": image,
8 }, {
9 headers: {
10 "Authorization": token,
11 }
12 });
13 return response.data.success;
14};
15
16export const post_map_summary = async (token: string, map_id: string, content: ModMenuContent): Promise<boolean> => {
17 const response = await axios.post(url(`maps/${map_id}/summary`), {
18 "category_id": content.category_id,
19 "user_name": content.name,
20 "score_count": content.score,
21 "record_date": content.date,
22 "showcase": content.showcase,
23 "description": content.description,
24 }, {
25 headers: {
26 "Authorization": token,
27 }
28 });
29 return response.data.success;
30};
31
32export const put_map_summary = async (token: string, map_id: string, content: ModMenuContent): Promise<boolean> => {
33 const response = await axios.put(url(`maps/${map_id}/summary`), {
34 "route_id": content.id,
35 "user_name": content.name,
36 "score_count": content.score,
37 "record_date": content.date,
38 "showcase": content.showcase,
39 "description": content.description,
40 }, {
41 headers: {
42 "Authorization": token,
43 }
44 });
45 return response.data.success;
46};
47
48export const delete_map_summary = async (token: string, map_id: string, route_id: number): Promise<boolean> => {
49 const response = await axios.delete(url(`maps/${map_id}/summary`), {
50 data: {
51 "route_id": route_id,
52 },
53 headers: {
54 "Authorization": token,
55 }
56 });
57 return response.data.success;
58};
diff --git a/frontend/src/api/Rankings.tsx b/frontend/src/api/Rankings.tsx
new file mode 100644
index 0000000..384f826
--- /dev/null
+++ b/frontend/src/api/Rankings.tsx
@@ -0,0 +1,13 @@
1import axios from "axios";
2import { url } from "./Api";
3import { Ranking, SteamRanking } from "../types/Ranking";
4
5export const get_official_rankings = async (): Promise<Ranking> => {
6 const response = await axios.get(url(`rankings/lphub`));
7 return response.data.data;
8};
9
10export const get_unofficial_rankings = async (): Promise<SteamRanking> => {
11 const response = await axios.get(url(`rankings/steam`));
12 return response.data.data;
13};
diff --git a/frontend/src/api/User.tsx b/frontend/src/api/User.tsx
new file mode 100644
index 0000000..c4d1944
--- /dev/null
+++ b/frontend/src/api/User.tsx
@@ -0,0 +1,25 @@
1import axios from "axios";
2import { url } from "./Api";
3import { UserProfile } from "../types/Profile";
4
5export const get_user = async (user_id: string): Promise<UserProfile> => {
6 const response = await axios.get(url(`users/${user_id}`));
7 return response.data.data;
8};
9
10export const get_profile = async (token: string): Promise<UserProfile> => {
11 const response = await axios.get(url(`profile`), {
12 headers: {
13 "Authorization": token,
14 }
15 });
16 return response.data.data;
17};
18
19export const post_profile = async (token: string) => {
20 const _ = await axios.post(url(`profile`), {}, {
21 headers: {
22 "Authorization": token,
23 }
24 });
25};
diff --git a/frontend/src/components/Discussions.tsx b/frontend/src/components/Discussions.tsx
index 787104b..c22de79 100644
--- a/frontend/src/components/Discussions.tsx
+++ b/frontend/src/components/Discussions.tsx
@@ -8,13 +8,14 @@ import "../css/Maps.css"
8import { Link } from 'react-router-dom'; 8import { Link } from 'react-router-dom';
9 9
10interface DiscussionsProps { 10interface DiscussionsProps {
11 token?: string
11 data?: MapDiscussions; 12 data?: MapDiscussions;
12 isModerator: boolean; 13 isModerator: boolean;
13 mapID: string; 14 mapID: string;
14 onRefresh: () => void; 15 onRefresh: () => void;
15} 16}
16 17
17const Discussions: React.FC<DiscussionsProps> = ({ data, isModerator, mapID, onRefresh }) => { 18const Discussions: React.FC<DiscussionsProps> = ({ token, data, isModerator, mapID, onRefresh }) => {
18 19
19 const [discussionThread, setDiscussionThread] = React.useState<MapDiscussion | undefined>(undefined); 20 const [discussionThread, setDiscussionThread] = React.useState<MapDiscussion | undefined>(undefined);
20 const [discussionSearch, setDiscussionSearch] = React.useState<string>(""); 21 const [discussionSearch, setDiscussionSearch] = React.useState<string>("");
@@ -24,9 +25,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ data, isModerator, mapID, onR
24 title: "", 25 title: "",
25 content: "", 26 content: "",
26 }); 27 });
27 const [createDiscussionCommentContent, setCreateDiscussionCommentContent] = React.useState<MapDiscussionCommentContent>({ 28 const [createDiscussionCommentContent, setCreateDiscussionCommentContent] = React.useState<string>("");
28 comment: "",
29 });
30 29
31 const _open_map_discussion = async (discussion_id: number) => { 30 const _open_map_discussion = async (discussion_id: number) => {
32 const mapDiscussion = await API.get_map_discussion(mapID, discussion_id); 31 const mapDiscussion = await API.get_map_discussion(mapID, discussion_id);
@@ -34,20 +33,26 @@ const Discussions: React.FC<DiscussionsProps> = ({ data, isModerator, mapID, onR
34 }; 33 };
35 34
36 const _create_map_discussion = async () => { 35 const _create_map_discussion = async () => {
37 await API.post_map_discussion(mapID, createDiscussionContent); 36 if (token) {
38 setCreateDiscussion(false); 37 await API.post_map_discussion(token, mapID, createDiscussionContent);
39 onRefresh(); 38 setCreateDiscussion(false);
39 onRefresh();
40 }
40 }; 41 };
41 42
42 const _create_map_discussion_comment = async (discussion_id: number) => { 43 const _create_map_discussion_comment = async (discussion_id: number) => {
43 await API.post_map_discussion_comment(mapID, discussion_id, createDiscussionCommentContent); 44 if (token) {
44 await _open_map_discussion(discussion_id); 45 await API.post_map_discussion_comment(token, mapID, discussion_id, createDiscussionCommentContent);
46 await _open_map_discussion(discussion_id);
47 }
45 }; 48 };
46 49
47 const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => { 50 const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => {
48 if (window.confirm(`Are you sure you want to remove post: ${discussion.title}?`)) { 51 if (window.confirm(`Are you sure you want to remove post: ${discussion.title}?`)) {
49 await API.delete_map_discussion(mapID, discussion.id); 52 if (token) {
50 onRefresh(); 53 await API.delete_map_discussion(token, mapID, discussion.id);
54 onRefresh();
55 }
51 } 56 }
52 }; 57 };
53 58
@@ -69,9 +74,9 @@ const Discussions: React.FC<DiscussionsProps> = ({ data, isModerator, mapID, onR
69 ...createDiscussionContent, 74 ...createDiscussionContent,
70 title: e.target.value, 75 title: e.target.value,
71 })} /> 76 })} />
72 <input id='discussion-create-content' placeholder='Enter the comment...' onChange={(e) => setCreateDiscussionContent({ 77 <input id='discussion-create-content' placeholder='Enter the content...' onChange={(e) => setCreateDiscussionContent({
73 ...createDiscussionContent, 78 ...createDiscussionContent,
74 title: e.target.value, 79 content: e.target.value,
75 })} /> 80 })} />
76 </div> 81 </div>
77 <div style={{ placeItems: "end", gridColumn: "1 / span 2" }}> 82 <div style={{ placeItems: "end", gridColumn: "1 / span 2" }}>
@@ -114,11 +119,15 @@ const Discussions: React.FC<DiscussionsProps> = ({ data, isModerator, mapID, onR
114 } 119 }
115 </div> 120 </div>
116 <div id='discussion-send'> 121 <div id='discussion-send'>
117 <input type="text" placeholder={"Message"} onKeyDown={(e) => e.key === "Enter" && _create_map_discussion_comment(discussionThread.discussion.id)} onChange={(e) => setCreateDiscussionCommentContent({ 122 <input type="text" value={createDiscussionCommentContent} placeholder={"Message"}
118 ...createDiscussionContent, 123 onKeyDown={(e) => e.key === "Enter" && _create_map_discussion_comment(discussionThread.discussion.id)}
119 comment: e.target.value, 124 onChange={(e) => setCreateDiscussionCommentContent(e.target.value)} />
120 })} /> 125 <div><button onClick={() => {
121 <div><button onClick={() => _create_map_discussion_comment(discussionThread.discussion.id)}>Send</button></div> 126 if (createDiscussionCommentContent !== "") {
127 _create_map_discussion_comment(discussionThread.discussion.id);
128 setCreateDiscussionCommentContent("");
129 }
130 }}>Send</button></div>
122 </div> 131 </div>
123 132
124 </div> 133 </div>
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx
index 159f3ed..f4d666d 100644
--- a/frontend/src/components/Leaderboards.tsx
+++ b/frontend/src/components/Leaderboards.tsx
@@ -88,14 +88,14 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ data }) => {
88 {r.kind === "multiplayer" ? ( 88 {r.kind === "multiplayer" ? (
89 <span> 89 <span>
90 <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> 90 <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>
91 <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> 91 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.partner_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(160deg) contrast(60%) saturate(1000%)" }} /></button>
92 <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> 92 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.host_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(300deg) contrast(60%) saturate(1000%)" }} /></button>
93 </span> 93 </span>
94 ) : r.kind === "singleplayer" && ( 94 ) : r.kind === "singleplayer" && (
95 95
96 <span> 96 <span>
97 <button onClick={() => { window.alert(`Demo ID: ${r.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 97 <button onClick={() => { window.alert(`Demo ID: ${r.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button>
98 <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${r.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 98 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.demo_id}`}><img src={DownloadIcon} alt="download" /></button>
99 </span> 99 </span>
100 )} 100 )}
101 </span> 101 </span>
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx
index 367dc72..a8c5503 100644
--- a/frontend/src/components/Login.tsx
+++ b/frontend/src/components/Login.tsx
@@ -7,22 +7,25 @@ import { API } from '../api/Api';
7import "../css/Login.css"; 7import "../css/Login.css";
8 8
9interface LoginProps { 9interface LoginProps {
10 token?: string;
11 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 10 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
12 profile?: UserProfile; 11 profile?: UserProfile;
13 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 12 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
14}; 13};
15 14
16const Login: React.FC<LoginProps> = ({ token, setToken, profile, setProfile }) => { 15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
17 16
18 const navigate = useNavigate(); 17 const navigate = useNavigate();
19 18
19 const _login = () => {
20 window.location.href = "/api/v1/login";
21 };
22
20 const _logout = () => { 23 const _logout = () => {
21 setProfile(undefined); 24 setProfile(undefined);
22 setToken(undefined); 25 setToken(undefined);
23 API.user_logout(); 26 API.delete_token();
24 navigate("/"); 27 navigate("/");
25 } 28 };
26 29
27 return ( 30 return (
28 <> 31 <>
@@ -30,1893 +33,41 @@ const Login: React.FC<LoginProps> = ({ token, setToken, profile, setProfile }) =
30 ? 33 ?
31 ( 34 (
32 <> 35 <>
33 <Link to="/profile" tabIndex={-1} className='login'> 36 {profile.profile ?
34 <button className='sidebar-button'> 37 (
35 <img src={profile.avatar_link} alt="" /> 38 <>
36 <span>{profile.user_name}</span> 39 <Link to="/profile" tabIndex={-1} className='login'>
37 </button> 40 <button className='sidebar-button'>
38 <button className='logout-button' onClick={_logout}> 41 <img src={profile.avatar_link} alt="" />
39 <img src={ExitIcon} alt="" /><span></span> 42 <span>{profile.user_name}</span>
40 </button> 43 </button>
41 </Link> 44 <button className='logout-button' onClick={_logout}>
45 <img src={ExitIcon} alt="" /><span />
46 </button>
47 </Link>
48 </>
49 )
50 :
51 (
52 <>
53 <Link to="/" tabIndex={-1} className='login'>
54 <button className='sidebar-button'>
55 <img src={profile.avatar_link} alt="" />
56 <span>Loading Profile...</span>
57 </button>
58 <button disabled className='logout-button' onClick={_logout}>
59 <img src={ExitIcon} alt="" /><span />
60 </button>
61 </Link>
62 </>
63 )
64 }
42 </> 65 </>
43 ) 66 )
44 : 67 :
45 ( 68 (
46 <Link to="/api/v1/login" tabIndex={-1} className='login' > 69 <Link to="/api/v1/login" tabIndex={-1} className='login' >
47 <button className='sidebar-button' onClick={() => { 70 <button className='sidebar-button' onClick={_login}>
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="" /> 71 <img src={UserIcon} alt="" />
1921 <span> 72 <span>
1922 <img src={LoginIcon} alt="Sign in through Steam" /> 73 <img src={LoginIcon} alt="Sign in through Steam" />
diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx
index a0945eb..5b0d1c8 100644
--- a/frontend/src/components/ModMenu.tsx
+++ b/frontend/src/components/ModMenu.tsx
@@ -5,14 +5,16 @@ import { MapSummary } from '../types/Map';
5import { ModMenuContent } from '../types/Content'; 5import { ModMenuContent } from '../types/Content';
6import { API } from '../api/Api'; 6import { API } from '../api/Api';
7import "../css/ModMenu.css" 7import "../css/ModMenu.css"
8import { useNavigate } from 'react-router-dom';
8 9
9interface ModMenuProps { 10interface ModMenuProps {
11 token?: string;
10 data: MapSummary; 12 data: MapSummary;
11 selectedRun: number; 13 selectedRun: number;
12 mapID: string; 14 mapID: string;
13} 15}
14 16
15const ModMenu: React.FC<ModMenuProps> = ({ data, selectedRun, mapID }) => { 17const ModMenu: React.FC<ModMenuProps> = ({ token, data, selectedRun, mapID }) => {
16 18
17 const [menu, setMenu] = React.useState<number>(0); 19 const [menu, setMenu] = React.useState<number>(0);
18 const [showButton, setShowButton] = React.useState<boolean>(true); 20 const [showButton, setShowButton] = React.useState<boolean>(true);
@@ -30,6 +32,8 @@ const ModMenu: React.FC<ModMenuProps> = ({ data, selectedRun, mapID }) => {
30 const [image, setImage] = React.useState<string>(""); 32 const [image, setImage] = React.useState<string>("");
31 const [md, setMd] = React.useState<string>(""); 33 const [md, setMd] = React.useState<string>("");
32 34
35 const navigate = useNavigate();
36
33 function compressImage(file: File): Promise<string> { 37 function compressImage(file: File): Promise<string> {
34 const reader = new FileReader(); 38 const reader = new FileReader();
35 reader.readAsDataURL(file); 39 reader.readAsDataURL(file);
@@ -61,26 +65,56 @@ const ModMenu: React.FC<ModMenuProps> = ({ data, selectedRun, mapID }) => {
61 65
62 const _edit_map_summary_image = async () => { 66 const _edit_map_summary_image = async () => {
63 if (window.confirm("Are you sure you want to submit this to the database?")) { 67 if (window.confirm("Are you sure you want to submit this to the database?")) {
64 await API.put_map_image(mapID, image); 68 if (token) {
69 const success = await API.put_map_image(token, mapID, image);
70 if (success) {
71 navigate(0);
72 } else {
73 alert("Error. Check logs.")
74 }
75 }
65 } 76 }
66 }; 77 };
67 78
68 const _edit_map_summary_route = async () => { 79 const _edit_map_summary_route = async () => {
69 if (window.confirm("Are you sure you want to submit this to the database?")) { 80 if (window.confirm("Are you sure you want to submit this to the database?")) {
70 await API.put_map_summary(mapID, routeContent); 81 if (token) {
82 routeContent.date += "T00:00:00Z";
83 const success = await API.put_map_summary(token, mapID, routeContent);
84 if (success) {
85 navigate(0);
86 } else {
87 alert("Error. Check logs.")
88 }
89 }
71 } 90 }
72 }; 91 };
73 92
74 const _create_map_summary_route = async () => { 93 const _create_map_summary_route = async () => {
75 if (window.confirm("Are you sure you want to submit this to the database?")) { 94 if (window.confirm("Are you sure you want to submit this to the database?")) {
76 await API.post_map_summary(mapID, routeContent); 95 if (token) {
96 routeContent.date += "T00:00:00Z";
97 const success = await API.post_map_summary(token, mapID, routeContent);
98 if (success) {
99 navigate(0);
100 } else {
101 alert("Error. Check logs.")
102 }
103 }
77 } 104 }
78 }; 105 };
79 106
80 const _delete_map_summary_route = async () => { 107 const _delete_map_summary_route = async () => {
81 if (window.confirm(`Are you sure you want to delete this run from the database? 108 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}`)) { 109 ${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); 110 if (token) {
111 const success = await API.delete_map_summary(token, mapID, data.summary.routes[selectedRun].route_id);
112 if (success) {
113 navigate(0);
114 } else {
115 alert("Error. Check logs.")
116 }
117 }
84 } 118 }
85 }; 119 };
86 120
diff --git a/frontend/src/components/RankingEntry.tsx b/frontend/src/components/RankingEntry.tsx
index 58a6aa6..25b0201 100644
--- a/frontend/src/components/RankingEntry.tsx
+++ b/frontend/src/components/RankingEntry.tsx
@@ -37,7 +37,7 @@ const RankingEntry: React.FC<RankingEntryProps> = (prop) => {
37 <span>{prop.curRankingData.user_name}</span> 37 <span>{prop.curRankingData.user_name}</span>
38 </Link> 38 </Link>
39 </div> 39 </div>
40 <span>{prop.curRankingData.overall_score}</span> 40 <span>{prop.currentLeaderboardType == RankingCategories.rankings_singleplayer ? prop.curRankingData.sp_score : prop.currentLeaderboardType == RankingCategories.rankings_multiplayer ? prop.curRankingData.mp_score : prop.curRankingData.overall_score}</span>
41 </div> 41 </div>
42 ) 42 )
43 } 43 }
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 22d5c8b..c5c91db 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -153,7 +153,7 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo
153 <span></span> 153 <span></span>
154 154
155 { 155 {
156 profile ? 156 profile && profile.profile ?
157 <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Submit&nbsp;a&nbsp;Run</span></button> 157 <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Submit&nbsp;a&nbsp;Run</span></button>
158 : 158 :
159 <span></span> 159 <span></span>
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx
index e099a40..aa7ab5a 100644
--- a/frontend/src/components/UploadRunDialog.tsx
+++ b/frontend/src/components/UploadRunDialog.tsx
@@ -42,8 +42,10 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ open, onClose, mapID,
42 }; 42 };
43 43
44 React.useEffect(() => { 44 React.useEffect(() => {
45 _handle_game_select("1"); // a different approach? 45 if (open) {
46 }, []); 46 _handle_game_select("1"); // a different approach?.
47 }
48 }, [open]);
47 49
48 if (open) { 50 if (open) {
49 return ( 51 return (
diff --git a/frontend/src/css/Profile.css b/frontend/src/css/Profile.css
index 4944ade..0829477 100644
--- a/frontend/src/css/Profile.css
+++ b/frontend/src/css/Profile.css
@@ -232,6 +232,12 @@ span.titles{
232 place-items: center; 232 place-items: center;
233 height: 44px; 233 height: 44px;
234} 234}
235.profileboard-record>a>span{
236
237 display: flex;
238 place-items: center;
239 height: 44px;
240}
235.profileboard-record>span>button{ 241.profileboard-record>span>button{
236 background-color: #0000; 242 background-color: #0000;
237 border: 0; 243 border: 0;
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx
index 8e5bb4b..8c1cd48 100644
--- a/frontend/src/pages/Homepage.tsx
+++ b/frontend/src/pages/Homepage.tsx
@@ -7,7 +7,7 @@ const Homepage: React.FC = () => {
7 <main> 7 <main>
8 <section> 8 <section>
9 <p/> 9 <p/>
10 <h1><img src={PortalIcon} alt="lphub"/>Welcome to Least Portals Hub!</h1> 10 <h1>Welcome to Least Portals Hub!</h1>
11 <p>At the moment, LPHUB is in beta state. This means that the site has only the core functionalities enabled for providing both collaborative information and competitive leaderboards.</p> 11 <p>At the moment, LPHUB is in beta state. This means that the site has only the core functionalities enabled for providing both collaborative information and competitive leaderboards.</p>
12 <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p> 12 <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p>
13 <p>By using LPHUB, you agree that you have read the 'Leaderboard Rules' and the 'About LPHUB' pages.</p> 13 <p>By using LPHUB, you agree that you have read the 'Leaderboard Rules' and the 'About LPHUB' pages.</p>
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx
index 5d0c852..9526d18 100644
--- a/frontend/src/pages/Maplist.tsx
+++ b/frontend/src/pages/Maplist.tsx
@@ -64,6 +64,7 @@ const Maplist: React.FC = () => {
64 // console.log(foundGame) 64 // console.log(foundGame)
65 if (foundGame) { 65 if (foundGame) {
66 setGame(foundGame); 66 setGame(foundGame);
67 setLoad(false);
67 } 68 }
68 }; 69 };
69 70
@@ -73,9 +74,9 @@ const Maplist: React.FC = () => {
73 setNumChapters(games_chapters.chapters.length); 74 setNumChapters(games_chapters.chapters.length);
74 } 75 }
75 76
77 setLoad(true);
76 _fetch_game(); 78 _fetch_game();
77 _fetch_game_chapters(); 79 _fetch_game_chapters();
78 setLoad(true);
79 }, []); 80 }, []);
80 81
81 useEffect(() => { 82 useEffect(() => {
@@ -96,7 +97,7 @@ const Maplist: React.FC = () => {
96 </button> 97 </button>
97 </Link> 98 </Link>
98 </section> 99 </section>
99 {!load ? ( 100 {load ? (
100 <div></div> 101 <div></div>
101 ) : ( 102 ) : (
102 <section> 103 <section>
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx
index 62fc3cc..13ee4d9 100644
--- a/frontend/src/pages/Maps.tsx
+++ b/frontend/src/pages/Maps.tsx
@@ -7,18 +7,16 @@ import Leaderboards from '../components/Leaderboards';
7import Discussions from '../components/Discussions'; 7import Discussions from '../components/Discussions';
8import ModMenu from '../components/ModMenu'; 8import ModMenu from '../components/ModMenu';
9import { MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map'; 9import { MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map';
10import { UserProfile } from '../types/Profile';
11import { API } from '../api/Api'; 10import { API } from '../api/Api';
12import "../css/Maps.css"; 11import "../css/Maps.css";
13import Loading from '../components/Loading'; 12import Loading from '../components/Loading';
14 13
15interface MapProps { 14interface MapProps {
16 profile?: UserProfile; 15 token?: string;
17 isModerator: boolean; 16 isModerator: boolean;
18 onUploadRun: (mapID: number) => void;
19}; 17};
20 18
21const Maps: React.FC<MapProps> = ({ profile, isModerator, onUploadRun }) => { 19const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
22 20
23 const [selectedRun, setSelectedRun] = React.useState<number>(0); 21 const [selectedRun, setSelectedRun] = React.useState<number>(0);
24 22
@@ -39,7 +37,8 @@ const Maps: React.FC<MapProps> = ({ profile, isModerator, onUploadRun }) => {
39 37
40 const _fetch_map_leaderboards = async () => { 38 const _fetch_map_leaderboards = async () => {
41 const mapLeaderboards = await API.get_map_leaderboard(mapID); 39 const mapLeaderboards = await API.get_map_leaderboard(mapID);
42 console.log(mapLeaderboards?.records[0]); 40 console.log("lbs:")
41 console.log(mapLeaderboards);
43 setMapLeaderboardData(mapLeaderboards); 42 setMapLeaderboardData(mapLeaderboards);
44 }; 43 };
45 44
@@ -77,7 +76,7 @@ const Maps: React.FC<MapProps> = ({ profile, isModerator, onUploadRun }) => {
77 76
78 return ( 77 return (
79 <> 78 <>
80 {isModerator && <ModMenu data={mapSummaryData} selectedRun={selectedRun} mapID={mapID} />} 79 {isModerator && <ModMenu token={token} data={mapSummaryData} selectedRun={selectedRun} mapID={mapID} />}
81 80
82 <div id='background-image'> 81 <div id='background-image'>
83 <img src={mapSummaryData.map.image} alt="" /> 82 <img src={mapSummaryData.map.image} alt="" />
@@ -88,7 +87,6 @@ const Maps: React.FC<MapProps> = ({ profile, isModerator, onUploadRun }) => {
88 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 0px 0px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> 87 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 0px 0px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link>
89 <Link to={`/games/${!mapSummaryData.map.is_coop ? "1" : "2"}?chapter=${mapSummaryData.map.chapter_name.split(" ")[1]}`}><button className='nav-button' style={{ borderRadius: "0px 20px 20px 0px", marginLeft: "2px" }}><i className='triangle'></i><span>{mapSummaryData.map.chapter_name}</span></button></Link> 88 <Link to={`/games/${!mapSummaryData.map.is_coop ? "1" : "2"}?chapter=${mapSummaryData.map.chapter_name.split(" ")[1]}`}><button className='nav-button' style={{ borderRadius: "0px 20px 20px 0px", marginLeft: "2px" }}><i className='triangle'></i><span>{mapSummaryData.map.chapter_name}</span></button></Link>
90 <br /><span><b>{mapSummaryData.map.map_name}</b></span> 89 <br /><span><b>{mapSummaryData.map.map_name}</b></span>
91 {profile && <button onClick={() => onUploadRun(mapSummaryData.map.id)}>Submit a Run</button>}
92 </div> 90 </div>
93 </section> 91 </section>
94 92
@@ -100,7 +98,7 @@ const Maps: React.FC<MapProps> = ({ profile, isModerator, onUploadRun }) => {
100 98
101 {navState === 0 && <Summary selectedRun={selectedRun} setSelectedRun={setSelectedRun} data={mapSummaryData} />} 99 {navState === 0 && <Summary selectedRun={selectedRun} setSelectedRun={setSelectedRun} data={mapSummaryData} />}
102 {navState === 1 && <Leaderboards data={mapLeaderboardData} />} 100 {navState === 1 && <Leaderboards data={mapLeaderboardData} />}
103 {navState === 2 && <Discussions data={mapDiscussionsData} isModerator={isModerator} mapID={mapID} onRefresh={() => _fetch_map_discussions()} />} 101 {navState === 2 && <Discussions data={mapDiscussionsData} token={token} isModerator={isModerator} mapID={mapID} onRefresh={() => _fetch_map_discussions()} />}
104 </main> 102 </main>
105 </> 103 </>
106 ); 104 );
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
index 8eec23c..e20d930 100644
--- a/frontend/src/pages/Profile.tsx
+++ b/frontend/src/pages/Profile.tsx
@@ -1,5 +1,5 @@
1import React from 'react'; 1import React from 'react';
2import { useLocation, useNavigate } from 'react-router-dom'; 2import { Link, useLocation, useNavigate } from 'react-router-dom';
3 3
4import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '../images/Images'; 4import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '../images/Images';
5import { UserProfile } from '../types/Profile'; 5import { UserProfile } from '../types/Profile';
@@ -7,104 +7,74 @@ import { Game, GameChapters } from '../types/Game';
7import { Map } from '../types/Map'; 7import { Map } from '../types/Map';
8import { ticks_to_time } from '../utils/Time'; 8import { ticks_to_time } from '../utils/Time';
9import "../css/Profile.css"; 9import "../css/Profile.css";
10import { API } from '../api/Api';
10 11
11interface ProfileProps { 12interface ProfileProps {
12 profile?: UserProfile; 13 profile?: UserProfile;
14 token?: string;
15 gameData: Game[];
13} 16}
14 17
15const Profile: React.FC<ProfileProps> = ({ profile }) => { 18const Profile: React.FC<ProfileProps> = ({ profile, token, gameData }) => {
16
17
18 const location = useLocation();
19 const navigate = useNavigate();
20
21 React.useEffect(() => {
22 if (!profile) {
23 navigate("/");
24 };
25 }, [profile]);
26 19
27 const [navState, setNavState] = React.useState(0); 20 const [navState, setNavState] = React.useState(0);
28 const [pageNumber, setPageNumber] = React.useState(1); 21 const [pageNumber, setPageNumber] = React.useState(1);
29 const [pageMax, setPageMax] = React.useState(0); 22 const [pageMax, setPageMax] = React.useState(0);
30 23
31 const [game, setGame] = React.useState("0") 24 const [game, setGame] = React.useState("0")
32 const [gameData, setGameData] = React.useState<Game[]>([]);
33 const [chapter, setChapter] = React.useState("0") 25 const [chapter, setChapter] = React.useState("0")
34 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); 26 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null);
35 const [maps, setMaps] = React.useState<Map[]>([]); 27 const [maps, setMaps] = React.useState<Map[]>([]);
36 28
37 function NavClick() { 29 const navigate = useNavigate();
38 if (profile) {
39 const btn = document.querySelectorAll("#section2 button");
40 btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" });
41 (btn[navState] as HTMLElement).style.backgroundColor = "#202232";
42 30
43 document.querySelectorAll("section").forEach((e, i) => i >= 2 ? e.style.display = "none" : "") 31 const _update_profile = () => {
44 if (navState === 0) { document.querySelectorAll(".profile1").forEach((e) => { (e as HTMLElement).style.display = "block" }); } 32 if (token) {
45 if (navState === 1) { document.querySelectorAll(".profile2").forEach((e) => { (e as HTMLElement).style.display = "block" }); } 33 API.post_profile(token).then(() => navigate(0));
46 } 34 }
47 } 35 };
48 36
49 function UpdateProfile() { 37 const _get_game_chapters = async () => {
50 fetch(`https://lp.ardapektezol.com/api/v1/profile`, { 38 if (game && game !== "0") {
51 method: 'POST', 39 const gameChapters = await API.get_games_chapters(game);
52 headers: { Authorization: "" } 40 setChapterData(gameChapters);
53 }).then(r => r.json()) 41 } else if (game && game === "0") {
54 .then(d => d.success ? window.alert("profile updated") : window.alert(`Error: ${d.message}`)) 42 setPageMax(Math.ceil(profile!.records.length / 20));
55 } 43 setPageNumber(1);
44 }
45 };
56 46
57 React.useEffect(() => { 47 const _get_game_maps = async () => {
58 if (profile) { 48 if (chapter === "0") {
59 fetch("https://lp.ardapektezol.com/api/v1/games") 49 const gameMaps = await API.get_game_maps(game);
60 .then(r => r.json()) 50 setMaps(gameMaps);
61 .then(d => { 51 setPageMax(Math.ceil(gameMaps.length / 20));
62 setGameData(d.data) 52 setPageNumber(1);
63 setGame("0") 53 } else {
64 }) 54 const gameChapters = await API.get_chapters(chapter);
55 setMaps(gameChapters.maps);
56 setPageMax(Math.ceil(gameChapters.maps.length / 20));
57 setPageNumber(1);
65 } 58 }
66 }, [profile, location]); 59 };
60
61 React.useEffect(() => {
62 if (!profile) {
63 navigate("/");
64 };
65 }, [profile]);
67 66
68 React.useEffect(() => { 67 React.useEffect(() => {
69 if (profile) { 68 if (profile) {
70 if (game && game !== "0") { 69 _get_game_chapters();
71 fetch(`https://lp.ardapektezol.com/api/v1/games/${game}`)
72 .then(r => r.json())
73 .then(d => {
74 setChapterData(d.data)
75 setChapter("0");
76 // (document.querySelector('#select-chapter') as HTMLInputElement).value = "0"
77 })
78
79 } else if (game && game === "0") {
80 setPageMax(Math.ceil(profile.records.length / 20))
81 setPageNumber(1)
82 }
83 } 70 }
84 }, [profile, game, location]); 71 }, [profile, game]);
85 72
86 React.useEffect(() => { 73 React.useEffect(() => {
87 if (game !== "0") { 74 if (profile && game !== "0") {
88 if (chapter === "0") { 75 _get_game_maps();
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 fetch(`https://lp.ardapektezol.com/api/v1/chapters/${chapter}`)
98 .then(r => r.json())
99 .then(d => {
100 setMaps(d.data.maps);
101 setPageMax(Math.ceil(d.data.maps.length / 20))
102 setPageNumber(1)
103 })
104
105 }
106 } 76 }
107 }, [game, chapter, chapterData]) 77 }, [profile, game, chapter, chapterData])
108 78
109 if (!profile) { 79 if (!profile) {
110 return ( 80 return (
@@ -118,7 +88,7 @@ const Profile: React.FC<ProfileProps> = ({ profile }) => {
118 88
119 {profile.profile 89 {profile.profile
120 ? ( 90 ? (
121 <div id='profile-image' onClick={() => UpdateProfile()}> 91 <div id='profile-image' onClick={_update_profile}>
122 <img src={profile.avatar_link} alt="profile-image"></img> 92 <img src={profile.avatar_link} alt="profile-image"></img>
123 <span>Refresh</span> 93 <span>Refresh</span>
124 </div> 94 </div>
@@ -187,7 +157,14 @@ const Profile: React.FC<ProfileProps> = ({ profile }) => {
187 {gameData === null ? <select>error</select> : 157 {gameData === null ? <select>error</select> :
188 158
189 <select id='select-game' 159 <select id='select-game'
190 onChange={() => setGame((document.querySelector('#select-game') as HTMLInputElement).value)}> 160 onChange={() => {
161 setGame((document.querySelector('#select-game') as HTMLInputElement).value);
162 setChapter("0");
163 const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement;
164 if (chapterSelect) {
165 chapterSelect.value = "0";
166 }
167 }}>
191 <option value={0} key={0}>All Scores</option> 168 <option value={0} key={0}>All Scores</option>
192 {gameData.map((e, i) => ( 169 {gameData.map((e, i) => (
193 <option value={e.id} key={i + 1}>{e.name}</option> 170 <option value={e.id} key={i + 1}>{e.name}</option>
@@ -240,7 +217,7 @@ const Profile: React.FC<ProfileProps> = ({ profile }) => {
240 {r.scores.map((e, i) => (<> 217 {r.scores.map((e, i) => (<>
241 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 218 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""}
242 219
243 <span>{r.map_name}</span> 220 <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link>
244 221
245 <span style={{ display: "grid" }}>{e.score_count}</span> 222 <span style={{ display: "grid" }}>{e.score_count}</span>
246 223
@@ -252,7 +229,7 @@ const Profile: React.FC<ProfileProps> = ({ profile }) => {
252 <span style={{ flexDirection: "row-reverse" }}> 229 <span style={{ flexDirection: "row-reverse" }}>
253 230
254 <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 231 <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button>
255 <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 232 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button>
256 {i === 0 && r.scores.length > 1 ? <button onClick={() => { 233 {i === 0 && r.scores.length > 1 ? <button onClick={() => {
257 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 234 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" ||
258 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 235 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ?
@@ -297,7 +274,7 @@ const Profile: React.FC<ProfileProps> = ({ profile }) => {
297 <span style={{ flexDirection: "row-reverse" }}> 274 <span style={{ flexDirection: "row-reverse" }}>
298 275
299 <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 276 <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button>
300 <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 277 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button>
301 {i === 0 && record!.scores.length > 1 ? <button onClick={() => { 278 {i === 0 && record!.scores.length > 1 ? <button onClick={() => {
302 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 279 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" ||
303 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 280 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ?
diff --git a/frontend/src/pages/Rankings.tsx b/frontend/src/pages/Rankings.tsx
index 6dbf3d3..9280b02 100644
--- a/frontend/src/pages/Rankings.tsx
+++ b/frontend/src/pages/Rankings.tsx
@@ -22,14 +22,8 @@ const Rankings: React.FC = () => {
22 const [currentLeaderboardType, setCurrentLeaderboardType] = React.useState<RankingCategories>(RankingCategories.rankings_singleplayer); 22 const [currentLeaderboardType, setCurrentLeaderboardType] = React.useState<RankingCategories>(RankingCategories.rankings_singleplayer);
23 const [load, setLoad] = React.useState<boolean>(false); 23 const [load, setLoad] = React.useState<boolean>(false);
24 24
25 interface ResponseSTUPID {
26 success: boolean;
27 message: string;
28 data: SteamRanking;
29 }
30
31 const _fetch_rankings = async () => { 25 const _fetch_rankings = async () => {
32 const rankings = await API.get_rankings(); 26 const rankings = await API.get_official_rankings();
33 setLeaderboardData(rankings); 27 setLeaderboardData(rankings);
34 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 28 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) {
35 setCurrentLeaderboard(rankings.rankings_singleplayer) 29 setCurrentLeaderboard(rankings.rankings_singleplayer)
@@ -43,20 +37,15 @@ const Rankings: React.FC = () => {
43 37
44 const __dev_fetch_unofficial_rankings = async () => { 38 const __dev_fetch_unofficial_rankings = async () => {
45 try { 39 try {
46 const response = await fetch("/response.json"); 40 const rankings = await API.get_unofficial_rankings();
47 const result: ResponseSTUPID = await response.json(); 41 setLeaderboardData(rankings);
48 42 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) {
49 if (result.success) { 43 // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer))
50 const unofficialRanking: SteamRanking = result.data; 44 setCurrentLeaderboard(rankings.rankings_singleplayer)
51 setLeaderboardData(unofficialRanking); 45 } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) {
52 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 46 setCurrentLeaderboard(rankings.rankings_multiplayer)
53 // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) 47 } else {
54 setCurrentLeaderboard(unofficialRanking.rankings_singleplayer) 48 setCurrentLeaderboard(rankings.rankings_overall)
55 } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) {
56 setCurrentLeaderboard(unofficialRanking.rankings_multiplayer)
57 } else {
58 setCurrentLeaderboard(unofficialRanking.rankings_overall)
59 }
60 } 49 }
61 } catch (e) { 50 } catch (e) {
62 console.log(e) 51 console.log(e)
@@ -79,7 +68,7 @@ const Rankings: React.FC = () => {
79 if (leaderboard_type == LeaderboardTypes.official) { 68 if (leaderboard_type == LeaderboardTypes.official) {
80 _fetch_rankings(); 69 _fetch_rankings();
81 } else { 70 } else {
82 71
83 } 72 }
84 } 73 }
85 74
@@ -117,23 +106,23 @@ const Rankings: React.FC = () => {
117 </section> 106 </section>
118 107
119 {load ? 108 {load ?
120 <section className="rankings-leaderboard"> 109 <section className="rankings-leaderboard">
121 <div className="ranks-container"> 110 <div className="ranks-container">
122 <div className="leaderboard-entry header"> 111 <div className="leaderboard-entry header">
123 <span>Rank</span> 112 <span>Rank</span>
124 <span>Player</span> 113 <span>Player</span>
125 <span>Portals</span> 114 <span>Portals</span>
115 </div>
116
117 <div className="splitter"></div>
118
119 {currentLeaderboard?.map((curRankingData, i) => {
120 return <RankingEntry currentLeaderboardType={currentLeaderboardType} curRankingData={curRankingData} key={i}></RankingEntry>
121 })
122 }
126 </div> 123 </div>
127 124 </section>
128 <div className="splitter"></div> 125 : null}
129
130 {currentLeaderboard?.map((curRankingData, i) => {
131 return <RankingEntry currentLeaderboardType={currentLeaderboardType} curRankingData={curRankingData} key={i}></RankingEntry>
132 })
133 }
134 </div>
135 </section>
136 : null}
137 </main> 126 </main>
138 ) 127 )
139} 128}
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx
index 516b73c..99fff1f 100644
--- a/frontend/src/pages/Rules.tsx
+++ b/frontend/src/pages/Rules.tsx
@@ -11,7 +11,7 @@ const Rules: React.FC = () => {
11 const fetchRules = async () => { 11 const fetchRules = async () => {
12 try { 12 try {
13 const response = await fetch( 13 const response = await fetch(
14 'https://raw.githubusercontent.com/pektezol/leastportalshub/main/README.md' 14 'https://raw.githubusercontent.com/pektezol/leastportalshub/typescript/RULES.md'
15 ); 15 );
16 if (!response.ok) { 16 if (!response.ok) {
17 throw new Error('Failed to fetch README'); 17 throw new Error('Failed to fetch README');
@@ -21,7 +21,7 @@ const Rules: React.FC = () => {
21 } catch (error) { 21 } catch (error) {
22 console.error('Error fetching Rules:', error); 22 console.error('Error fetching Rules:', error);
23 } 23 }
24 setRulesText(rulesText) 24 // setRulesText(rulesText)
25 }; 25 };
26 fetchRules(); 26 fetchRules();
27 }, []); 27 }, []);
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx
index 1605ada..e3781a3 100644
--- a/frontend/src/pages/User.tsx
+++ b/frontend/src/pages/User.tsx
@@ -1,5 +1,5 @@
1import React from 'react'; 1import React from 'react';
2import { useLocation } from 'react-router-dom'; 2import { Link, useLocation, useNavigate } from 'react-router-dom';
3 3
4import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '../images/Images'; 4import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '../images/Images';
5import { UserProfile } from '../types/Profile'; 5import { UserProfile } from '../types/Profile';
@@ -9,8 +9,13 @@ import { API } from '../api/Api';
9import { ticks_to_time } from '../utils/Time'; 9import { ticks_to_time } from '../utils/Time';
10import "../css/Profile.css"; 10import "../css/Profile.css";
11 11
12const User: React.FC = () => { 12interface UserProps {
13 const location = useLocation(); 13 profile?: UserProfile;
14 token?: string;
15 gameData: Game[];
16}
17
18const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
14 19
15 const [user, setUser] = React.useState<UserProfile | undefined>(undefined); 20 const [user, setUser] = React.useState<UserProfile | undefined>(undefined);
16 21
@@ -18,91 +23,63 @@ const User: React.FC = () => {
18 const [pageNumber, setPageNumber] = React.useState(1); 23 const [pageNumber, setPageNumber] = React.useState(1);
19 const [pageMax, setPageMax] = React.useState(0); 24 const [pageMax, setPageMax] = React.useState(0);
20 25
21 const [game, setGame] = React.useState("0") 26 const [game, setGame] = React.useState("0");
22 const [gameData, setGameData] = React.useState<Game[]>([]); 27 const [chapter, setChapter] = React.useState("0");
23 const [chapter, setChapter] = React.useState("0")
24 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); 28 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null);
25 const [maps, setMaps] = React.useState<Map[]>([]); 29 const [maps, setMaps] = React.useState<Map[]>([]);
26 30
27 function NavClick() { 31 const location = useLocation();
28 if (user) { 32 const navigate = useNavigate();
29 const btn = document.querySelectorAll("#section2 button");
30 btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" });
31 (btn[navState] as HTMLElement).style.backgroundColor = "#202232";
32 33
33 document.querySelectorAll("section").forEach((e, i) => i >= 2 ? e.style.display = "none" : "") 34 const _fetch_user = async () => {
34 if (navState === 0) { document.querySelectorAll(".profile1").forEach((e) => { (e as HTMLElement).style.display = "block" }); } 35 const userID = location.pathname.split("/")[2];
35 if (navState === 1) { document.querySelectorAll(".profile2").forEach((e) => { (e as HTMLElement).style.display = "block" }); } 36 if (token && profile && profile.profile && profile.steam_id === userID) {
37 navigate("/profile");
38 return;
36 } 39 }
37 } 40 const userData = await API.get_user(userID);
41 setUser(userData);
42 };
38 43
39 function UpdateProfile() { 44 const _get_game_chapters = async () => {
40 fetch(`https://lp.ardapektezol.com/api/v1/profile`, { 45 if (game !== "0") {
41 method: 'POST', 46 const gameChapters = await API.get_games_chapters(game);
42 headers: { Authorization: "" } 47 setChapterData(gameChapters);
43 }).then(r => r.json()) 48 } else {
44 .then(d => d.success ? window.alert("profile updated") : window.alert(`Error: ${d.message}`)) 49 setPageMax(Math.ceil(user!.records.length / 20));
45 } 50 setPageNumber(1);
51 }
52 };
46 53
47 const _fetch_user = async () => { 54 const _get_game_maps = async () => {
48 const userData = await API.get_user(location.pathname.split("/")[2]); 55 if (chapter === "0") {
49 setUser(userData); 56 const gameMaps = await API.get_game_maps(game);
57 setMaps(gameMaps);
58 setPageMax(Math.ceil(gameMaps.length / 20));
59 setPageNumber(1);
60 } else {
61 const gameChapters = await API.get_chapters(chapter);
62 setMaps(gameChapters.maps);
63 setPageMax(Math.ceil(gameChapters.maps.length / 20));
64 setPageNumber(1);
65 }
50 }; 66 };
51 67
52 React.useEffect(() => { 68 React.useEffect(() => {
53 fetch("https://lp.ardapektezol.com/api/v1/games") 69 _fetch_user();
54 .then(r => r.json())
55 .then(d => {
56 setGameData(d.data)
57 setGame("0")
58 })
59
60 }, [location]); 70 }, [location]);
61 71
62 React.useEffect(() => { 72 React.useEffect(() => {
63 if (user) { 73 if (user) {
64 if (game && game !== "0") { 74 _get_game_chapters();
65 fetch(`https://lp.ardapektezol.com/api/v1/games/${game}`)
66 .then(r => r.json())
67 .then(d => {
68 setChapterData(d.data)
69 setChapter("0");
70 // (document.querySelector('#select-chapter') as HTMLInputElement).value = "0"
71 })
72
73 } else if (game && game === "0") {
74 setPageMax(Math.ceil(user.records.length / 20))
75 setPageNumber(1)
76 }
77 } 75 }
78 }, [user, game, location]); 76 }, [user, game, location]);
79 77
80 React.useEffect(() => { 78 React.useEffect(() => {
81 _fetch_user(); 79 if (user && game !== "0") {
82 }, [user]); 80 _get_game_maps();
83
84 React.useEffect(() => {
85 if (game !== "0") {
86 if (chapter === "0") {
87 fetch(`https://lp.ardapektezol.com/api/v1/games/${game}/maps`)
88 .then(r => r.json())
89 .then(d => {
90 setMaps(d.data.maps);
91 setPageMax(Math.ceil(d.data.maps.length / 20))
92 setPageNumber(1)
93 })
94 } else {
95 fetch(`https://lp.ardapektezol.com/api/v1/chapters/${chapter}`)
96 .then(r => r.json())
97 .then(d => {
98 setMaps(d.data.maps);
99 setPageMax(Math.ceil(d.data.maps.length / 20))
100 setPageNumber(1)
101 })
102
103 }
104 } 81 }
105 }, [game, chapter, chapterData]) 82 }, [user, game, chapter, location])
106 83
107 if (!user) { 84 if (!user) {
108 return ( 85 return (
@@ -113,19 +90,9 @@ const User: React.FC = () => {
113 return ( 90 return (
114 <main> 91 <main>
115 <section id='section1' className='profile'> 92 <section id='section1' className='profile'>
116 93 <div>
117 {user.profile 94 <img src={user.avatar_link} alt="profile-image"></img>
118 ? ( 95 </div>
119 <div id='profile-image' onClick={() => UpdateProfile()}>
120 <img src={user.avatar_link} alt="profile-image"></img>
121 <span>Refresh</span>
122 </div>
123 ) : (
124 <div>
125 <img src={user.avatar_link} alt="profile-image"></img>
126 </div>
127 )}
128
129 <div id='profile-top'> 96 <div id='profile-top'>
130 <div> 97 <div>
131 <div>{user.user_name}</div> 98 <div>{user.user_name}</div>
@@ -185,7 +152,14 @@ const User: React.FC = () => {
185 {gameData === null ? <select>error</select> : 152 {gameData === null ? <select>error</select> :
186 153
187 <select id='select-game' 154 <select id='select-game'
188 onChange={() => setGame((document.querySelector('#select-game') as HTMLInputElement).value)}> 155 onChange={() => {
156 setGame((document.querySelector('#select-game') as HTMLInputElement).value);
157 setChapter("0");
158 const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement;
159 if (chapterSelect) {
160 chapterSelect.value = "0";
161 }
162 }}>
189 <option value={0} key={0}>All Scores</option> 163 <option value={0} key={0}>All Scores</option>
190 {gameData.map((e, i) => ( 164 {gameData.map((e, i) => (
191 <option value={e.id} key={i + 1}>{e.name}</option> 165 <option value={e.id} key={i + 1}>{e.name}</option>
@@ -238,7 +212,7 @@ const User: React.FC = () => {
238 {r.scores.map((e, i) => (<> 212 {r.scores.map((e, i) => (<>
239 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 213 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""}
240 214
241 <span>{r.map_name}</span> 215 <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link>
242 216
243 <span style={{ display: "grid" }}>{e.score_count}</span> 217 <span style={{ display: "grid" }}>{e.score_count}</span>
244 218
@@ -250,7 +224,7 @@ const User: React.FC = () => {
250 <span style={{ flexDirection: "row-reverse" }}> 224 <span style={{ flexDirection: "row-reverse" }}>
251 225
252 <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 226 <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button>
253 <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 227 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button>
254 {i === 0 && r.scores.length > 1 ? <button onClick={() => { 228 {i === 0 && r.scores.length > 1 ? <button onClick={() => {
255 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 229 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" ||
256 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 230 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ?
@@ -295,7 +269,7 @@ const User: React.FC = () => {
295 <span style={{ flexDirection: "row-reverse" }}> 269 <span style={{ flexDirection: "row-reverse" }}>
296 270
297 <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 271 <button onClick={() => { window.alert(`Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button>
298 <button onClick={() => window.location.href = `https://lp.ardapektezol.com/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 272 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button>
299 {i === 0 && record!.scores.length > 1 ? <button onClick={() => { 273 {i === 0 && record!.scores.length > 1 ? <button onClick={() => {
300 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 274 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" ||
301 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 275 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ?
diff --git a/frontend/src/utils/Jwt.tsx b/frontend/src/utils/Jwt.tsx
new file mode 100644
index 0000000..ce351fb
--- /dev/null
+++ b/frontend/src/utils/Jwt.tsx
@@ -0,0 +1,44 @@
1// llm ahh funcs
2export function get_user_id_from_token(token: string | undefined): string | undefined {
3 if (!token) {
4 return undefined;
5 }
6 const parts = token.split('.');
7 if (parts.length !== 3) {
8 return undefined;
9 }
10 const base64Url = parts[1];
11 const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
12
13 const jsonPayload = decodeURIComponent(
14 atob(base64)
15 .split('')
16 .map(function (c) {
17 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
18 })
19 .join('')
20 );
21 return JSON.parse(jsonPayload).sub;
22};
23
24export function get_user_mod_from_token(token: string | undefined): boolean | undefined {
25 if (!token) {
26 return undefined;
27 }
28 const parts = token.split('.');
29 if (parts.length !== 3) {
30 return undefined;
31 }
32 const base64Url = parts[1];
33 const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
34
35 const jsonPayload = decodeURIComponent(
36 atob(base64)
37 .split('')
38 .map(function (c) {
39 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
40 })
41 .join('')
42 );
43 return JSON.parse(jsonPayload).mod;
44};
diff --git a/rankings/input/records.json b/rankings/input/records.json
index 276310f..884fd49 100644
--- a/rankings/input/records.json
+++ b/rankings/input/records.json
@@ -335,7 +335,7 @@
335 "id": 47830, 335 "id": 47830,
336 "name": "Rat Maze", 336 "name": "Rat Maze",
337 "mode": 2, 337 "mode": 2,
338 "wr": 2 338 "wr": 0
339 }, 339 },
340 { 340 {
341 "id": 45466, 341 "id": 45466,
@@ -401,7 +401,7 @@
401 "id": 47847, 401 "id": 47847,
402 "name": "Cooperative Bridges", 402 "name": "Cooperative Bridges",
403 "mode": 2, 403 "mode": 2,
404 "wr": 3 404 "wr": 2
405 }, 405 },
406 { 406 {
407 "id": 47850, 407 "id": 47850,
@@ -431,7 +431,7 @@
431 "id": 47860, 431 "id": 47860,
432 "name": "Turret Walls", 432 "name": "Turret Walls",
433 "mode": 2, 433 "mode": 2,
434 "wr": 4 434 "wr": 2
435 }, 435 },
436 { 436 {
437 "id": 52641, 437 "id": 52641,
@@ -509,7 +509,7 @@
509 "id": 52712, 509 "id": 52712,
510 "name": "Double Bounce", 510 "name": "Double Bounce",
511 "mode": 2, 511 "mode": 2,
512 "wr": 2 512 "wr": 0
513 }, 513 },
514 { 514 {
515 "id": 52713, 515 "id": 52713,
diff --git a/rankings/main.go b/rankings/main.go
index dfafb0c..70a25e9 100644
--- a/rankings/main.go
+++ b/rankings/main.go
@@ -2,12 +2,8 @@ package main
2 2
3import ( 3import (
4 "log" 4 "log"
5 "os"
6 "os/signal"
7 "syscall"
8 5
9 "github.com/joho/godotenv" 6 "github.com/joho/godotenv"
10 "github.com/robfig/cron/v3"
11) 7)
12 8
13func main() { 9func main() {
@@ -15,16 +11,17 @@ func main() {
15 if err != nil { 11 if err != nil {
16 log.Fatalln("Error loading .env file:", err.Error()) 12 log.Fatalln("Error loading .env file:", err.Error())
17 } 13 }
18 c := cron.New() 14 run()
19 _, err = c.AddFunc("0 0 * * *", run) 15 // c := cron.New()
20 if err != nil { 16 // _, err = c.AddFunc("0 0 * * *", run)
21 log.Fatalln("Error scheduling daily reminder:", err.Error()) 17 // if err != nil {
22 } 18 // log.Fatalln("Error scheduling daily reminder:", err.Error())
23 c.Start() 19 // }
24 log.Println("ready for jobs") 20 // c.Start()
25 sc := make(chan os.Signal, 1) 21 // log.Println("ready for jobs")
26 signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) 22 // sc := make(chan os.Signal, 1)
27 <-sc 23 // signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
24 // <-sc
28} 25}
29 26
30func run() { 27func run() {