aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWolfboy248 <georgejvindkarlsen@gmail.com>2024-10-28 11:43:38 +0100
committerWolfboy248 <georgejvindkarlsen@gmail.com>2024-10-28 11:43:38 +0100
commit6fdc20a4db98531545badedf983a81a05f0ea450 (patch)
tree69d314fd8fad33cc12c676617b5bc1c6ff066cce
parentrefactor: uploadrundialog (diff)
parentbackend: fix user completion count (diff)
downloadlphub-6fdc20a4db98531545badedf983a81a05f0ea450.tar.gz
lphub-6fdc20a4db98531545badedf983a81a05f0ea450.tar.bz2
lphub-6fdc20a4db98531545badedf983a81a05f0ea450.zip
Merge branch 'typescript' of https://github.com/pektezol/LeastPortalsHub into typescript
-rw-r--r--README.md2
-rw-r--r--RULES.md1
-rw-r--r--backend/docs/docs.go2
-rw-r--r--backend/docs/index.html2
-rw-r--r--backend/docs/swagger.json2
-rw-r--r--backend/docs/swagger.yaml2
-rw-r--r--backend/handlers/record.go2
-rw-r--r--backend/handlers/user.go54
-rw-r--r--backend/main.go4
-rw-r--r--frontend/src/App.tsx28
-rw-r--r--frontend/src/api/Maps.tsx6
-rw-r--r--frontend/src/components/Leaderboards.tsx6
-rw-r--r--frontend/src/components/MessageDialog.tsx2
-rw-r--r--frontend/src/components/UploadRunDialog.tsx32
-rw-r--r--frontend/src/css/Dialog.css3
-rw-r--r--frontend/src/css/UploadRunDialog.css7
-rw-r--r--frontend/src/hooks/UseConfirm.tsx2
-rw-r--r--frontend/src/hooks/UseMessage.tsx6
-rw-r--r--frontend/src/pages/Profile.tsx7
19 files changed, 83 insertions, 87 deletions
diff --git a/README.md b/README.md
index 5ff180e..6d1eef5 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ There's also discussion tabs for each map to act like forum pages in order to ta
34 34
35## Documentation 35## Documentation
36 36
37Full API documentation can be found at https://lp.ardapektezol.com/api/v1/ 37Full API documentation can be found at https://lp.pektezol.dev/api/v1/
38 38
39## License 39## License
40 40
diff --git a/RULES.md b/RULES.md
index 3645d8f..ffd344d 100644
--- a/RULES.md
+++ b/RULES.md
@@ -22,6 +22,7 @@ Both `.vtf` and `.vmt` files associated with these textures can be modified.
22- Proof of your run must be recorded using demos. To start recording, type `record <demoname>` when entering the map, or use the SAR plugin with the command `sar_record_at 0`. 22- Proof of your run must be recorded using demos. To start recording, type `record <demoname>` when entering the map, or use the SAR plugin with the command `sar_record_at 0`.
23- A valid demo begins when the player can move and ends when the player touches the end flags. 23- A valid demo begins when the player can move and ends when the player touches the end flags.
24- For cooperative runs, both players must submit their demos. 24- For cooperative runs, both players must submit their demos.
25- For technical reasons, there is a 250 MB limit for demo files. If you have a demo file bigger than 250 MB that you need to submit, please contact an admin in the Discord server.
25 26
26### Console Commands & Cheats 27### Console Commands & Cheats
27 28
diff --git a/backend/docs/docs.go b/backend/docs/docs.go
index 5327961..71bd68e 100644
--- a/backend/docs/docs.go
+++ b/backend/docs/docs.go
@@ -2218,7 +2218,7 @@ const docTemplate = `{
2218// SwaggerInfo holds exported Swagger Info so clients can modify it 2218// SwaggerInfo holds exported Swagger Info so clients can modify it
2219var SwaggerInfo = &swag.Spec{ 2219var SwaggerInfo = &swag.Spec{
2220 Version: "1.0", 2220 Version: "1.0",
2221 Host: "lp.ardapektezol.com", 2221 Host: "lp.pektezol.dev",
2222 BasePath: "/api/v1", 2222 BasePath: "/api/v1",
2223 Schemes: []string{}, 2223 Schemes: []string{},
2224 Title: "Least Portals Hub", 2224 Title: "Least Portals Hub",
diff --git a/backend/docs/index.html b/backend/docs/index.html
index 8d12274..997b7d6 100644
--- a/backend/docs/index.html
+++ b/backend/docs/index.html
@@ -17,7 +17,7 @@
17 <script> 17 <script>
18 window.onload = () => { 18 window.onload = () => {
19 window.ui = SwaggerUIBundle({ 19 window.ui = SwaggerUIBundle({
20 url: 'https://lp.ardapektezol.com/swagger.json', 20 url: '/swagger.json',
21 dom_id: '#swagger-ui', 21 dom_id: '#swagger-ui',
22 }); 22 });
23 }; 23 };
diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json
index 94a0213..879f35f 100644
--- a/backend/docs/swagger.json
+++ b/backend/docs/swagger.json
@@ -10,7 +10,7 @@
10 }, 10 },
11 "version": "1.0" 11 "version": "1.0"
12 }, 12 },
13 "host": "lp.ardapektezol.com", 13 "host": "lp.pektezol.dev",
14 "basePath": "/api/v1", 14 "basePath": "/api/v1",
15 "paths": { 15 "paths": {
16 "/chapters/{chapterid}": { 16 "/chapters/{chapterid}": {
diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml
index 26d28b1..2dee421 100644
--- a/backend/docs/swagger.yaml
+++ b/backend/docs/swagger.yaml
@@ -545,7 +545,7 @@ definitions:
545 user_name: 545 user_name:
546 type: string 546 type: string
547 type: object 547 type: object
548host: lp.ardapektezol.com 548host: lp.pektezol.dev
549info: 549info:
550 contact: {} 550 contact: {}
551 description: Backend API endpoints for Least Portals Hub. 551 description: Backend API endpoints for Least Portals Hub.
diff --git a/backend/handlers/record.go b/backend/handlers/record.go
index c720823..e43cc61 100644
--- a/backend/handlers/record.go
+++ b/backend/handlers/record.go
@@ -204,7 +204,7 @@ func CreateRecordWithDemo(c *gin.Context) {
204 // c.JSON(http.StatusOK, models.ErrorResponse(fmt.Sprintf("Partner SteamID from demo and request does not match! Check your submission and try again.\nDemo Partner SteamID: %s\nRequest Partner SteamID: %s", convertedPartnerSteamID, record.PartnerID))) 204 // c.JSON(http.StatusOK, models.ErrorResponse(fmt.Sprintf("Partner SteamID from demo and request does not match! Check your submission and try again.\nDemo Partner SteamID: %s\nRequest Partner SteamID: %s", convertedPartnerSteamID, record.PartnerID)))
205 // return 205 // return
206 // } 206 // }
207 if convertedHostSteamID != user.(models.User).SteamID || convertedPartnerSteamID != user.(models.User).SteamID { 207 if convertedHostSteamID != user.(models.User).SteamID && convertedPartnerSteamID != user.(models.User).SteamID {
208 deleteFile(srv, hostDemoFileID) 208 deleteFile(srv, hostDemoFileID)
209 deleteFile(srv, partnerDemoFileID) 209 deleteFile(srv, partnerDemoFileID)
210 c.JSON(http.StatusOK, models.ErrorResponse("You are permitted to only upload your own runs!")) 210 c.JSON(http.StatusOK, models.ErrorResponse("You are permitted to only upload your own runs!"))
diff --git a/backend/handlers/user.go b/backend/handlers/user.go
index 17a7819..dc058c8 100644
--- a/backend/handlers/user.go
+++ b/backend/handlers/user.go
@@ -98,19 +98,19 @@ func Profile(c *gin.Context) {
98 } 98 }
99 rankings.Overall.CompletionTotal = rankings.Singleplayer.CompletionTotal + rankings.Cooperative.CompletionTotal 99 rankings.Overall.CompletionTotal = rankings.Singleplayer.CompletionTotal + rankings.Cooperative.CompletionTotal
100 // Get user completion count 100 // Get user completion count
101 sql = `SELECT 'records_sp' AS table_name, COUNT(sp.id) 101 sql = `SELECT 'records_sp' AS table_name, COUNT(*) FROM (
102 FROM records_sp sp JOIN ( 102 SELECT sp.map_id FROM records_sp sp JOIN (
103 SELECT mh.map_id, MIN(mh.score_count) AS min_score_count 103 SELECT mh.map_id, MIN(mh.score_count) AS min_score_count FROM map_history mh WHERE mh.category_id = 1 GROUP BY mh.map_id
104 FROM public.map_history mh WHERE mh.category_id = 1 GROUP BY mh.map_id 104 ) AS subquery_sp ON sp.map_id = subquery_sp.map_id AND sp.score_count = subquery_sp.min_score_count
105 ) AS subquery_sp ON sp.map_id = subquery_sp.map_id AND sp.score_count = subquery_sp.min_score_count 105 WHERE sp.user_id = $1 AND sp.is_deleted = false GROUP BY sp.map_id
106 WHERE sp.user_id = $1 AND sp.is_deleted = false 106 ) AS unique_maps
107 UNION ALL 107 UNION ALL
108 SELECT 'records_mp' AS table_name, COUNT(mp.id) 108 SELECT 'records_mp' AS table_name, COUNT(*) FROM (
109 FROM public.records_mp mp JOIN ( 109 SELECT mp.map_id FROM records_mp mp JOIN (
110 SELECT mh.map_id, MIN(mh.score_count) AS min_score_count 110 SELECT mh.map_id, MIN(mh.score_count) AS min_score_count FROM map_history mh WHERE mh.category_id = 1 GROUP BY mh.map_id
111 FROM public.map_history mh WHERE mh.category_id = 1 GROUP BY mh.map_id 111 ) AS subquery_mp ON mp.map_id = subquery_mp.map_id AND mp.score_count = subquery_mp.min_score_count
112 ) AS subquery_mp ON mp.map_id = subquery_mp.map_id AND mp.score_count = subquery_mp.min_score_count 112 WHERE (mp.host_id = $1 OR mp.partner_id = $1) AND mp.is_deleted = false GROUP BY mp.map_id
113 WHERE (mp.host_id = $1 OR mp.partner_id = $1) AND mp.is_deleted = false` 113 ) AS unique_maps`
114 rows, err := database.DB.Query(sql, user.(models.User).SteamID) 114 rows, err := database.DB.Query(sql, user.(models.User).SteamID)
115 if err != nil { 115 if err != nil {
116 c.JSON(http.StatusOK, models.ErrorResponse(err.Error())) 116 c.JSON(http.StatusOK, models.ErrorResponse(err.Error()))
@@ -350,7 +350,7 @@ func Profile(c *gin.Context) {
350 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) 350 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score)
351 } 351 }
352 // Get multiplayer records 352 // Get multiplayer records
353 sql = `SELECT mp.id, m.game_id, m.chapter_id, mp.map_id, m."name", (SELECT mh.score_count FROM map_history mh WHERE mh.map_id = mp.map_id ORDER BY mh.score_count ASC LIMIT 1) AS wr_count, mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date 353 sql = `SELECT mp.id, m.game_id, m.chapter_id, mp.map_id, m."name", (SELECT mh.score_count FROM map_history mh WHERE mh.map_id = mp.map_id AND mh.category_id = 1 ORDER BY mh.score_count ASC LIMIT 1) AS wr_count, mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date
354 FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE (mp.host_id = $1 OR mp.partner_id = $1) AND mp.is_deleted = false ORDER BY mp.map_id, mp.score_count, mp.score_time` 354 FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE (mp.host_id = $1 OR mp.partner_id = $1) AND mp.is_deleted = false ORDER BY mp.map_id, mp.score_count, mp.score_time`
355 rows, err = database.DB.Query(sql, user.(models.User).SteamID) 355 rows, err = database.DB.Query(sql, user.(models.User).SteamID)
356 if err != nil { 356 if err != nil {
@@ -476,19 +476,19 @@ func FetchUser(c *gin.Context) {
476 } 476 }
477 rankings.Overall.CompletionTotal = rankings.Singleplayer.CompletionTotal + rankings.Cooperative.CompletionTotal 477 rankings.Overall.CompletionTotal = rankings.Singleplayer.CompletionTotal + rankings.Cooperative.CompletionTotal
478 // Get user completion count 478 // Get user completion count
479 sql = `SELECT 'records_sp' AS table_name, COUNT(sp.id) 479 sql = `SELECT 'records_sp' AS table_name, COUNT(*) FROM (
480 FROM records_sp sp JOIN ( 480 SELECT sp.map_id FROM records_sp sp JOIN (
481 SELECT mh.map_id, MIN(mh.score_count) AS min_score_count 481 SELECT mh.map_id, MIN(mh.score_count) AS min_score_count FROM map_history mh WHERE mh.category_id = 1 GROUP BY mh.map_id
482 FROM public.map_history mh WHERE mh.category_id = 1 GROUP BY mh.map_id 482 ) AS subquery_sp ON sp.map_id = subquery_sp.map_id AND sp.score_count = subquery_sp.min_score_count
483 ) AS subquery_sp ON sp.map_id = subquery_sp.map_id AND sp.score_count = subquery_sp.min_score_count 483 WHERE sp.user_id = $1 AND sp.is_deleted = false GROUP BY sp.map_id
484 WHERE sp.user_id = $1 AND sp.is_deleted = false 484 ) AS unique_maps
485 UNION ALL 485 UNION ALL
486 SELECT 'records_mp' AS table_name, COUNT(mp.id) 486 SELECT 'records_mp' AS table_name, COUNT(*) FROM (
487 FROM public.records_mp mp JOIN ( 487 SELECT mp.map_id FROM records_mp mp JOIN (
488 SELECT mh.map_id, MIN(mh.score_count) AS min_score_count 488 SELECT mh.map_id, MIN(mh.score_count) AS min_score_count FROM map_history mh WHERE mh.category_id = 1 GROUP BY mh.map_id
489 FROM public.map_history mh WHERE mh.category_id = 1 GROUP BY mh.map_id 489 ) AS subquery_mp ON mp.map_id = subquery_mp.map_id AND mp.score_count = subquery_mp.min_score_count
490 ) AS subquery_mp ON mp.map_id = subquery_mp.map_id AND mp.score_count = subquery_mp.min_score_count 490 WHERE (mp.host_id = $1 OR mp.partner_id = $1) AND mp.is_deleted = false GROUP BY mp.map_id
491 WHERE (mp.host_id = $1 OR mp.partner_id = $1) AND mp.is_deleted = false` 491 ) AS unique_maps`
492 rows, err = database.DB.Query(sql, user.SteamID) 492 rows, err = database.DB.Query(sql, user.SteamID)
493 if err != nil { 493 if err != nil {
494 c.JSON(http.StatusOK, models.ErrorResponse(err.Error())) 494 c.JSON(http.StatusOK, models.ErrorResponse(err.Error()))
@@ -672,7 +672,7 @@ func FetchUser(c *gin.Context) {
672 } 672 }
673 records := []ProfileRecords{} 673 records := []ProfileRecords{}
674 // Get singleplayer records 674 // Get singleplayer records
675 sql = `SELECT sp.id, m.game_id, m.chapter_id, sp.map_id, m."name", (SELECT mh.score_count FROM map_history mh WHERE mh.map_id = sp.map_id ORDER BY mh.score_count ASC LIMIT 1) AS wr_count, sp.score_count, sp.score_time, sp.demo_id, sp.record_date 675 sql = `SELECT sp.id, m.game_id, m.chapter_id, sp.map_id, m."name", (SELECT mh.score_count FROM map_history mh WHERE mh.map_id = sp.map_id AND mh.category_id = 1 ORDER BY mh.score_count ASC LIMIT 1) AS wr_count, sp.score_count, sp.score_time, sp.demo_id, sp.record_date
676 FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 AND sp.is_deleted = false ORDER BY sp.map_id, sp.score_count, sp.score_time` 676 FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 AND sp.is_deleted = false ORDER BY sp.map_id, sp.score_count, sp.score_time`
677 rows, err = database.DB.Query(sql, user.SteamID) 677 rows, err = database.DB.Query(sql, user.SteamID)
678 if err != nil { 678 if err != nil {
@@ -728,7 +728,7 @@ func FetchUser(c *gin.Context) {
728 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) 728 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score)
729 } 729 }
730 // Get multiplayer records 730 // Get multiplayer records
731 sql = `SELECT mp.id, m.game_id, m.chapter_id, mp.map_id, m."name", (SELECT mh.score_count FROM map_history mh WHERE mh.map_id = mp.map_id ORDER BY mh.score_count ASC LIMIT 1) AS wr_count, mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date 731 sql = `SELECT mp.id, m.game_id, m.chapter_id, mp.map_id, m."name", (SELECT mh.score_count FROM map_history mh WHERE mh.map_id = mp.map_id AND mh.category_id = 1 ORDER BY mh.score_count ASC LIMIT 1) AS wr_count, mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date
732 FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE (mp.host_id = $1 OR mp.partner_id = $1) AND mp.is_deleted = false ORDER BY mp.map_id, mp.score_count, mp.score_time` 732 FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE (mp.host_id = $1 OR mp.partner_id = $1) AND mp.is_deleted = false ORDER BY mp.map_id, mp.score_count, mp.score_time`
733 rows, err = database.DB.Query(sql, user.SteamID) 733 rows, err = database.DB.Query(sql, user.SteamID)
734 if err != nil { 734 if err != nil {
diff --git a/backend/main.go b/backend/main.go
index 26aa852..a1a4a20 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -20,7 +20,7 @@ import (
20// @license.name GNU Affero General Public License, Version 3 20// @license.name GNU Affero General Public License, Version 3
21// @license.url https://www.gnu.org/licenses/agpl-3.0.html 21// @license.url https://www.gnu.org/licenses/agpl-3.0.html
22 22
23// @host lp.ardapektezol.com 23// @host lp.pektezol.dev
24// @BasePath /api/v1 24// @BasePath /api/v1
25func main() { 25func main() {
26 err := godotenv.Load() 26 err := godotenv.Load()
@@ -47,6 +47,6 @@ func main() {
47 // router.NoRoute(func(c *gin.Context) { 47 // router.NoRoute(func(c *gin.Context) {
48 // c.File("../frontend/build/index.html") 48 // c.File("../frontend/build/index.html")
49 // }) 49 // })
50 router.MaxMultipartMemory = 500 << 20 // 500 mb limit for demos 50 router.MaxMultipartMemory = 250 << 20 // 250 mb limit for demos
51 router.Run(fmt.Sprintf(":%s", os.Getenv("PORT"))) 51 router.Run(fmt.Sprintf(":%s", os.Getenv("PORT")))
52} 52}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index d1d501d..c6952b1 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, useLocation } from "react-router-dom"; 2import { Routes, Route } 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';
@@ -18,22 +18,15 @@ import { 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'; 20import { get_user_id_from_token, get_user_mod_from_token } from './utils/Jwt';
21import { MapDeleteEndpoint } from './types/Map';
22 21
23const App: React.FC = () => { 22const App: React.FC = () => {
24 const [token, setToken] = React.useState<string | undefined>(undefined); 23 const [token, setToken] = React.useState<string | undefined>(undefined);
25 const [profile, setProfile] = React.useState<UserProfile | undefined>(undefined); 24 const [profile, setProfile] = React.useState<UserProfile | undefined>(undefined);
26 const [isModerator, setIsModerator] = React.useState<boolean>(false); 25 const [isModerator, setIsModerator] = React.useState<boolean>(false);
27 26
28 const [msgIsOpen, setMsgIsOpen] = React.useState<boolean>(false);
29
30 const [games, setGames] = React.useState<Game[]>([]); 27 const [games, setGames] = React.useState<Game[]>([]);
31 28
32 const [uploadRunDialog, setUploadRunDialog] = React.useState<boolean>(false); 29 const [uploadRunDialog, setUploadRunDialog] = React.useState<boolean>(false);
33 const [uploadRunDialogMapID, setUploadRunDialogMapID] = React.useState<number | undefined>(undefined);
34
35 const [confirmDialogOpen, setConfirmDialogOpen] = React.useState<boolean>(false);
36 const [currDeleteMapInfo, setCurrDeleteMapInfo] = React.useState<MapDeleteEndpoint>();
37 30
38 const _fetch_token = async () => { 31 const _fetch_token = async () => {
39 const token = await API.get_token(); 32 const token = await API.get_token();
@@ -45,10 +38,9 @@ const App: React.FC = () => {
45 setGames(games); 38 setGames(games);
46 }; 39 };
47 40
48 const _set_profile = async (user_id: string | undefined) => { 41 const _set_profile = async (user_id?: string) => {
49 if (user_id) { 42 if (user_id && token) {
50 setProfile({} as UserProfile); // placeholder before we call actual user profile 43 const user = await API.get_profile(token);
51 const user = await API.get_profile(token!);
52 setProfile(user); 44 setProfile(user);
53 } 45 }
54 }; 46 };
@@ -58,6 +50,7 @@ const App: React.FC = () => {
58 setProfile(undefined); 50 setProfile(undefined);
59 setIsModerator(false); 51 setIsModerator(false);
60 } else { 52 } else {
53 setProfile({} as UserProfile); // placeholder before we call actual user profile
61 _set_profile(get_user_id_from_token(token)) 54 _set_profile(get_user_id_from_token(token))
62 const modStatus = get_user_mod_from_token(token) 55 const modStatus = get_user_mod_from_token(token)
63 if (modStatus) { 56 if (modStatus) {
@@ -81,15 +74,20 @@ const App: React.FC = () => {
81 74
82 return ( 75 return (
83 <> 76 <>
84 <UploadRunDialog token={token} open={uploadRunDialog} onClose={() => setUploadRunDialog(false)} games={games} /> 77 <UploadRunDialog token={token} open={uploadRunDialog} onClose={(updateProfile) => {
78 setUploadRunDialog(false);
79 if (updateProfile) {
80 _set_profile(get_user_id_from_token(token));
81 }
82 }} games={games} />
85 <Sidebar setToken={setToken} profile={profile} setProfile={setProfile} onUploadRun={() => setUploadRunDialog(true)} /> 83 <Sidebar setToken={setToken} profile={profile} setProfile={setProfile} onUploadRun={() => setUploadRunDialog(true)} />
86 <Routes> 84 <Routes>
87 <Route path="/" element={<Homepage />} /> 85 <Route path="/" element={<Homepage />} />
88 <Route path="/profile" element={<Profile profile={profile} token={token} gameData={games} onDeleteRecord={() => setConfirmDialogOpen(true)} />} /> 86 <Route path="/profile" element={<Profile profile={profile} token={token} gameData={games} onDeleteRecord={() => _set_profile(get_user_id_from_token(token))} />} />
89 <Route path="/users/*" element={<User profile={profile} token={token} gameData={games} />} /> 87 <Route path="/users/*" element={<User profile={profile} token={token} gameData={games} />} />
90 <Route path="/games" element={<Games games={games} />} /> 88 <Route path="/games" element={<Games games={games} />} />
91 <Route path='/games/:id' element={<Maplist />}></Route> 89 <Route path='/games/:id' element={<Maplist />}></Route>
92 <Route path="/maps/*" element={<Maps token={token} isModerator={isModerator} />}/> 90 <Route path="/maps/*" element={<Maps token={token} isModerator={isModerator} />} />
93 <Route path="/rules" element={<Rules />} /> 91 <Route path="/rules" element={<Rules />} />
94 <Route path="/about" element={<About />} /> 92 <Route path="/about" element={<About />} />
95 <Route path='/rankings' element={<Rankings />}></Route> 93 <Route path='/rankings' element={<Rankings />}></Route>
diff --git a/frontend/src/api/Maps.tsx b/frontend/src/api/Maps.tsx
index 2209788..80f88d4 100644
--- a/frontend/src/api/Maps.tsx
+++ b/frontend/src/api/Maps.tsx
@@ -75,7 +75,7 @@ export const delete_map_discussion = async (token: string, map_id: string, discu
75 return response.data.success; 75 return response.data.success;
76}; 76};
77 77
78export const post_record = async (token: string, run: UploadRunContent): Promise<string> => { 78export const post_record = async (token: string, run: UploadRunContent): Promise<[boolean, string]> => {
79 if (run.partner_demo) { 79 if (run.partner_demo) {
80 const response = await axios.postForm(url(`maps/${run.map_id}/record`), { 80 const response = await axios.postForm(url(`maps/${run.map_id}/record`), {
81 "host_demo": run.host_demo, 81 "host_demo": run.host_demo,
@@ -85,7 +85,7 @@ export const post_record = async (token: string, run: UploadRunContent): Promise
85 "Authorization": token, 85 "Authorization": token,
86 } 86 }
87 }); 87 });
88 return response.data.message; 88 return [ response.data.success, response.data.message ];
89 } else { 89 } else {
90 const response = await axios.postForm(url(`maps/${run.map_id}/record`), { 90 const response = await axios.postForm(url(`maps/${run.map_id}/record`), {
91 "host_demo": run.host_demo, 91 "host_demo": run.host_demo,
@@ -94,7 +94,7 @@ export const post_record = async (token: string, run: UploadRunContent): Promise
94 "Authorization": token, 94 "Authorization": token,
95 } 95 }
96 }); 96 });
97 return response.data.message; 97 return [ response.data.success, response.data.message ];
98 } 98 }
99} 99}
100 100
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx
index f4d666d..f86aa7b 100644
--- a/frontend/src/components/Leaderboards.tsx
+++ b/frontend/src/components/Leaderboards.tsx
@@ -40,8 +40,8 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ data }) => {
40 40
41 {data.map.is_coop ? ( 41 {data.map.is_coop ? (
42 <div id='runner'> 42 <div id='runner'>
43 <span>Host</span> 43 <span>Blue</span>
44 <span>Partner</span> 44 <span>Orange</span>
45 </div> 45 </div>
46 ) : ( 46 ) : (
47 <span>Runner</span> 47 <span>Runner</span>
@@ -87,7 +87,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ data }) => {
87 87
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 = `/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 = `/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>
diff --git a/frontend/src/components/MessageDialog.tsx b/frontend/src/components/MessageDialog.tsx
index 0a8db42..17b1258 100644
--- a/frontend/src/components/MessageDialog.tsx
+++ b/frontend/src/components/MessageDialog.tsx
@@ -19,7 +19,7 @@ const MessageDialog: React.FC<MessageDialogProps> = ({ title, subtitle, onClose
19 <span>{subtitle}</span> 19 <span>{subtitle}</span>
20 </div> 20 </div>
21 <div className='dialog-element dialog-btns-container'> 21 <div className='dialog-element dialog-btns-container'>
22 <button onClick={onClose}>Ok</button> 22 <button onClick={onClose}>Close</button>
23 </div> 23 </div>
24 </div> 24 </div>
25 </div> 25 </div>
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx
index 8081643..a0d23e7 100644
--- a/frontend/src/components/UploadRunDialog.tsx
+++ b/frontend/src/components/UploadRunDialog.tsx
@@ -13,18 +13,15 @@ import useConfirm from '../hooks/UseConfirm';
13interface UploadRunDialogProps { 13interface UploadRunDialogProps {
14 token?: string; 14 token?: string;
15 open: boolean; 15 open: boolean;
16 onClose: () => void; 16 onClose: (updateProfile: boolean) => void;
17 games: Game[]; 17 games: Game[];
18} 18}
19 19
20const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, games }) => { 20const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, games }) => {
21 21
22 const [confirmMessage, setConfirmMessage] = React.useState<string>("Are you sure you want to upload this demo?");
23
24 const { message, MessageDialogComponent } = useMessage(); 22 const { message, MessageDialogComponent } = useMessage();
25 const { confirm, ConfirmDialogComponent } = useConfirm(); 23 const { confirm, ConfirmDialogComponent } = useConfirm();
26 24
27
28 const navigate = useNavigate(); 25 const navigate = useNavigate();
29 26
30 const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ 27 const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({
@@ -125,44 +122,41 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
125 if (token) { 122 if (token) {
126 if (games[selectedGameID].is_coop) { 123 if (games[selectedGameID].is_coop) {
127 if (uploadRunContent.host_demo === null) { 124 if (uploadRunContent.host_demo === null) {
128 message("Error", "You must select a host demo to upload.") 125 await message("Error", "You must select a host demo to upload.")
129 return 126 return
130 } else if (uploadRunContent.partner_demo === null) { 127 } else if (uploadRunContent.partner_demo === null) {
131 message("Error", "You must select a partner demo to upload.") 128 await message("Error", "You must select a partner demo to upload.")
132 return 129 return
133 } 130 }
134 } else { 131 } else {
135 if (uploadRunContent.host_demo === null) { 132 if (uploadRunContent.host_demo === null) {
136 message("Error", "You must select a demo to upload.") 133 await message("Error", "You must select a demo to upload.")
137 return 134 return
138 } 135 }
139 } 136 }
140 const demo = SourceDemoParser.default() 137 const demo = SourceDemoParser.default()
141 .setOptions({ packets: true, header: true }) 138 .setOptions({ packets: true, header: true })
142 .parse(await uploadRunContent.host_demo.arrayBuffer()); 139 .parse(await uploadRunContent.host_demo.arrayBuffer());
143 const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>((message) => { 140 const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>((msg) => {
144 return message instanceof NetMessages.SvcUserMessage && message.userMessage instanceof ScoreboardTempUpdate; 141 return msg instanceof NetMessages.SvcUserMessage && msg.userMessage instanceof ScoreboardTempUpdate;
145 }) 142 })
146 143
147 if (!scoreboard) { 144 if (!scoreboard) {
148 message("Error", "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode.") 145 await message("Error", "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode.")
149 return 146 return
150 } 147 }
151 const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; 148 const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {};
152 console.log(`Map Name: ${demo.mapName}. Portal Count: ${portalScore}. Ticks: ${timeScore}.`);
153
154 setConfirmMessage(`Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`)
155 149
156 const userConfirmed = await confirm("Upload demo?", confirmMessage); 150 const userConfirmed = await confirm("Upload Record", `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`);
157 151
158 if (!userConfirmed) { 152 if (!userConfirmed) {
159 return; 153 return;
160 } 154 }
161 155
162 const response = await API.post_record(token, uploadRunContent); 156 const [ success, response ] = await API.post_record(token, uploadRunContent);
163 await message("Message", response); 157 await message("Upload Record", response);
164 // navigate(0); 158 console.log("weweew")
165 onClose(); 159 onClose(success);
166 } 160 }
167 }; 161 };
168 162
@@ -242,7 +236,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
242 </div> 236 </div>
243 <div className='upload-run-buttons-container'> 237 <div className='upload-run-buttons-container'>
244 <button onClick={_upload_run}>Submit</button> 238 <button onClick={_upload_run}>Submit</button>
245 <button onClick={() => onClose()}>Cancel</button> 239 <button onClick={() => onClose(false)}>Cancel</button>
246 </div> 240 </div>
247 </div> 241 </div>
248 </> 242 </>
diff --git a/frontend/src/css/Dialog.css b/frontend/src/css/Dialog.css
index f16425d..47f070b 100644
--- a/frontend/src/css/Dialog.css
+++ b/frontend/src/css/Dialog.css
@@ -1,10 +1,9 @@
1.dimmer { 1.dimmer {
2 position: fixed; 2 position: fixed;
3 width: calc(100%); 3 width: 100%;
4 height: 100%; 4 height: 100%;
5 background-color: rgba(0, 0, 0, 0.5); 5 background-color: rgba(0, 0, 0, 0.5);
6 z-index: 4; 6 z-index: 4;
7 left: 0px;
8} 7}
9 8
10.dialog { 9.dialog {
diff --git a/frontend/src/css/UploadRunDialog.css b/frontend/src/css/UploadRunDialog.css
index b4667da..c783d2a 100644
--- a/frontend/src/css/UploadRunDialog.css
+++ b/frontend/src/css/UploadRunDialog.css
@@ -13,7 +13,7 @@ div#upload-run{
13 left: calc(50% + 160px); top: 130px; 13 left: calc(50% + 160px); top: 130px;
14 transform: translateX(-50%); 14 transform: translateX(-50%);
15 background-color: #2b2e46; 15 background-color: #2b2e46;
16 z-index: 2; color: white; 16 z-index: 3; color: white;
17 font-family: BarlowSemicondensed-SemiBold; 17 font-family: BarlowSemicondensed-SemiBold;
18} 18}
19 19
@@ -36,10 +36,9 @@ div#upload-run{
36 position: absolute; 36 position: absolute;
37 background-color: black; 37 background-color: black;
38 opacity: .3; 38 opacity: .3;
39 left: 320px; 39 width: 100%;
40 width: calc(100% - 320px);
41 height: 100%; 40 height: 100%;
42 z-index: 2; 41 z-index: 3;
43 cursor: no-drop; 42 cursor: no-drop;
44} 43}
45 44
diff --git a/frontend/src/hooks/UseConfirm.tsx b/frontend/src/hooks/UseConfirm.tsx
index 80a0d51..9a7853b 100644
--- a/frontend/src/hooks/UseConfirm.tsx
+++ b/frontend/src/hooks/UseConfirm.tsx
@@ -3,9 +3,9 @@ import ConfirmDialog from '../components/ConfirmDialog';
3 3
4const useConfirm = () => { 4const useConfirm = () => {
5 const [isOpen, setIsOpen] = useState(false); 5 const [isOpen, setIsOpen] = useState(false);
6 const [resolvePromise, setResolvePromise] = useState<((value: boolean) => void) | null>(null);
7 const [title, setTitle] = useState<string>(""); 6 const [title, setTitle] = useState<string>("");
8 const [subtitle, setSubtitle] = useState<string>(""); 7 const [subtitle, setSubtitle] = useState<string>("");
8 const [resolvePromise, setResolvePromise] = useState<((value: boolean) => void) | null>(null);
9 9
10 const confirm = ( titleN: string, subtitleN: string ) => { 10 const confirm = ( titleN: string, subtitleN: string ) => {
11 setIsOpen(true); 11 setIsOpen(true);
diff --git a/frontend/src/hooks/UseMessage.tsx b/frontend/src/hooks/UseMessage.tsx
index ebc4276..602cf65 100644
--- a/frontend/src/hooks/UseMessage.tsx
+++ b/frontend/src/hooks/UseMessage.tsx
@@ -3,10 +3,10 @@ import MessageDialog from "../components/MessageDialog";
3 3
4const useMessage = () => { 4const useMessage = () => {
5 const [isOpen, setIsOpen] = useState(false); 5 const [isOpen, setIsOpen] = useState(false);
6 const [resolvePromise, setResolvePromise] = useState<((value: boolean) => void) | null>(null);
7 6
8 const [title, setTitle] = useState<string>(""); 7 const [title, setTitle] = useState<string>("");
9 const [subtitle, setSubtitle] = useState<string>(""); 8 const [subtitle, setSubtitle] = useState<string>("");
9 const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(null);
10 10
11 const message = (title: string, subtitle: string) => { 11 const message = (title: string, subtitle: string) => {
12 setIsOpen(true); 12 setIsOpen(true);
@@ -19,6 +19,10 @@ const useMessage = () => {
19 19
20 const handleClose = () => { 20 const handleClose = () => {
21 setIsOpen(false); 21 setIsOpen(false);
22 if (resolvePromise) {
23 resolvePromise();
24 setResolvePromise(null);
25 }
22 }; 26 };
23 27
24 const MessageDialogComponent = isOpen && ( 28 const MessageDialogComponent = isOpen && (
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
index 7559c77..5d1c75d 100644
--- a/frontend/src/pages/Profile.tsx
+++ b/frontend/src/pages/Profile.tsx
@@ -63,7 +63,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
63 }; 63 };
64 64
65 const _delete_submission = async (map_id: number, record_id: number) => { 65 const _delete_submission = async (map_id: number, record_id: number) => {
66 const userConfirmed = await confirm("Delete record?", "This action cannot be undone"); 66 const userConfirmed = await confirm("Delete Record", "Are you sure you want to delete this record?");
67 67
68 if (!userConfirmed) { 68 if (!userConfirmed) {
69 return; 69 return;
@@ -71,9 +71,10 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
71 71
72 const api_success = await API.delete_map_record(token!, map_id, record_id); 72 const api_success = await API.delete_map_record(token!, map_id, record_id);
73 if (api_success) { 73 if (api_success) {
74 message("Success", "Successfully deleted record"); 74 await message("Delete Record", "Successfully deleted record.");
75 onDeleteRecord();
75 } else { 76 } else {
76 message("Error", "Could not delete record"); 77 await message("Delete Record", "Could not delete record.");
77 } 78 }
78 }; 79 };
79 80