diff options
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | RULES.md | 1 | ||||
| -rw-r--r-- | backend/docs/docs.go | 2 | ||||
| -rw-r--r-- | backend/docs/index.html | 2 | ||||
| -rw-r--r-- | backend/docs/swagger.json | 2 | ||||
| -rw-r--r-- | backend/docs/swagger.yaml | 2 | ||||
| -rw-r--r-- | backend/handlers/record.go | 2 | ||||
| -rw-r--r-- | backend/handlers/user.go | 54 | ||||
| -rw-r--r-- | backend/main.go | 4 | ||||
| -rw-r--r-- | frontend/src/App.tsx | 28 | ||||
| -rw-r--r-- | frontend/src/api/Maps.tsx | 6 | ||||
| -rw-r--r-- | frontend/src/components/Leaderboards.tsx | 6 | ||||
| -rw-r--r-- | frontend/src/components/MessageDialog.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/components/UploadRunDialog.tsx | 32 | ||||
| -rw-r--r-- | frontend/src/css/Dialog.css | 3 | ||||
| -rw-r--r-- | frontend/src/css/UploadRunDialog.css | 7 | ||||
| -rw-r--r-- | frontend/src/hooks/UseConfirm.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/hooks/UseMessage.tsx | 6 | ||||
| -rw-r--r-- | frontend/src/pages/Profile.tsx | 7 |
19 files changed, 83 insertions, 87 deletions
| @@ -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 | ||
| 37 | Full API documentation can be found at https://lp.ardapektezol.com/api/v1/ | 37 | Full API documentation can be found at https://lp.pektezol.dev/api/v1/ |
| 38 | 38 | ||
| 39 | ## License | 39 | ## License |
| 40 | 40 | ||
| @@ -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 |
| 2219 | var SwaggerInfo = &swag.Spec{ | 2219 | var 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 |
| 548 | host: lp.ardapektezol.com | 548 | host: lp.pektezol.dev |
| 549 | info: | 549 | info: |
| 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 |
| 25 | func main() { | 25 | func 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 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | import { Routes, Route, useLocation } from "react-router-dom"; | 2 | import { Routes, Route } from "react-router-dom"; |
| 3 | 3 | ||
| 4 | import { UserProfile } from './types/Profile'; | 4 | import { UserProfile } from './types/Profile'; |
| 5 | import Sidebar from './components/Sidebar'; | 5 | import Sidebar from './components/Sidebar'; |
| @@ -18,22 +18,15 @@ import { API } from './api/Api'; | |||
| 18 | import Maplist from './pages/Maplist'; | 18 | import Maplist from './pages/Maplist'; |
| 19 | import Rankings from './pages/Rankings'; | 19 | import Rankings from './pages/Rankings'; |
| 20 | import { get_user_id_from_token, get_user_mod_from_token } from './utils/Jwt'; | 20 | import { get_user_id_from_token, get_user_mod_from_token } from './utils/Jwt'; |
| 21 | import { MapDeleteEndpoint } from './types/Map'; | ||
| 22 | 21 | ||
| 23 | const App: React.FC = () => { | 22 | const 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 | ||
| 78 | export const post_record = async (token: string, run: UploadRunContent): Promise<string> => { | 78 | export 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'; | |||
| 13 | interface UploadRunDialogProps { | 13 | interface 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 | ||
| 20 | const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, games }) => { | 20 | const 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 | ||
| 4 | const useConfirm = () => { | 4 | const 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 | ||
| 4 | const useMessage = () => { | 4 | const 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 | ||