aboutsummaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/App.css5
-rw-r--r--frontend/src/App.tsx34
-rw-r--r--frontend/src/api/Api.tsx15
-rw-r--r--frontend/src/components/Loading.tsx11
-rw-r--r--frontend/src/components/Login.tsx2
-rw-r--r--frontend/src/components/Sidebar.tsx33
-rw-r--r--frontend/src/components/UploadRunDialog.tsx103
-rw-r--r--frontend/src/css/About.css16
-rw-r--r--frontend/src/css/Maplist.css2
-rw-r--r--frontend/src/css/Maps.css3
-rw-r--r--frontend/src/css/Sidebar.css65
-rw-r--r--frontend/src/css/UploadRunDialog.css47
-rw-r--r--frontend/src/images/Images.tsx4
-rw-r--r--frontend/src/images/png/20.pngbin0 -> 2072 bytes
-rw-r--r--frontend/src/pages/About.tsx36
-rw-r--r--frontend/src/pages/Games.tsx15
-rw-r--r--frontend/src/pages/Homepage.tsx1
-rw-r--r--frontend/src/pages/Maplist.tsx11
-rw-r--r--frontend/src/pages/Maps.tsx33
-rw-r--r--frontend/src/types/Content.tsx8
20 files changed, 394 insertions, 50 deletions
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 3b732f0..b0445d8 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -15,6 +15,11 @@ main {
15 15
16} 16}
17 17
18a {
19 color: inherit;
20 width: fit-content;
21}
22
18body { 23body {
19 overflow: hidden; 24 overflow: hidden;
20 background-color: #141520; 25 background-color: #141520;
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index d45cd97..095cbbe 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -10,6 +10,10 @@ import Games from './pages/Games';
10import Maps from './pages/Maps'; 10import Maps from './pages/Maps';
11import User from './pages/User'; 11import User from './pages/User';
12import Homepage from './pages/Homepage'; 12import Homepage from './pages/Homepage';
13import UploadRunDialog from './components/UploadRunDialog';
14import About from './pages/About';
15import { Game } from './types/Game';
16import { API } from './api/Api';
13import Maplist from './pages/Maplist'; 17import Maplist from './pages/Maplist';
14import Rankings from './pages/Rankings'; 18import Rankings from './pages/Rankings';
15 19
@@ -18,22 +22,44 @@ const App: React.FC = () => {
18 const [profile, setProfile] = React.useState<UserProfile | undefined>(undefined); 22 const [profile, setProfile] = React.useState<UserProfile | undefined>(undefined);
19 const [isModerator, setIsModerator] = React.useState<boolean>(true); 23 const [isModerator, setIsModerator] = React.useState<boolean>(true);
20 24
25 const [games, setGames] = React.useState<Game[]>([]);
26
27 const [uploadRunDialog, setUploadRunDialog] = React.useState<boolean>(false);
28 const [uploadRunDialogMapID, setUploadRunDialogMapID] = React.useState<number | undefined>(undefined);
29
21 // React.useEffect(() => { 30 // React.useEffect(() => {
22 // if (token) { 31 // if (token) {
23 // setIsModerator(JSON.parse(atob(token.split(".")[1])).mod) 32 // setIsModerator(JSON.parse(atob(token.split(".")[1])).mod)
24 // } 33 // }
25 // }, [token]); 34 // }, [token]);
26 35
36 const _fetch_games = async () => {
37 const games = await API.get_games();
38 setGames(games);
39 };
40
41 React.useEffect(() => {
42 _fetch_games();
43 }, []);
44
45 if (!games) {
46 return (
47 <></>
48 )
49 };
50
27 return ( 51 return (
28 <> 52 <>
29 <Sidebar setToken={setToken} profile={profile} setProfile={setProfile} /> 53 <UploadRunDialog open={uploadRunDialog} onClose={() => setUploadRunDialog(false)} mapID={uploadRunDialogMapID} games={games} />
54 <Sidebar setToken={setToken} profile={profile} setProfile={setProfile} onUploadRun={() => setUploadRunDialog(true)} />
30 <Routes> 55 <Routes>
31 <Route path="/" element={<Homepage />} /> 56 <Route path="/" element={<Homepage />} />
32 <Route path="/profile" element={<Profile profile={profile} />} /> 57 <Route path="/profile" element={<Profile profile={profile} />} />
33 <Route path="/users/*" element={<User />} /> 58 <Route path="/users/*" element={<User />} />
34 <Route path="/games" element={<Games />} /> 59 <Route path="/games" element={<Games games={games} />} />
35 <Route path="/maps/*" element={<Maps isModerator={isModerator} />} /> 60 <Route path='/games/:id' element={<Maplist />}></Route>
36 <Route path='/games/:id' element={<Maplist></Maplist>}></Route> 61 <Route path="/maps/*" element={<Maps profile={profile} isModerator={isModerator} onUploadRun={(mapID) => {setUploadRunDialog(true);setUploadRunDialogMapID(mapID)}} />}/>
62 <Route path="/about" element={<About />} />
37 <Route path='/rankings' element={<Rankings></Rankings>}></Route> 63 <Route path='/rankings' element={<Rankings></Rankings>}></Route>
38 <Route path="*" element={"404"} /> 64 <Route path="*" element={"404"} />
39 </Routes> 65 </Routes>
diff --git a/frontend/src/api/Api.tsx b/frontend/src/api/Api.tsx
index e62bb22..1f77088 100644
--- a/frontend/src/api/Api.tsx
+++ b/frontend/src/api/Api.tsx
@@ -1,8 +1,8 @@
1import axios from 'axios'; 1import axios from 'axios';
2 2
3import { Game, GameChapters } from '../types/Game';
4import { GameChapter, GamesChapters } from '../types/Chapters'; 3import { GameChapter, GamesChapters } from '../types/Chapters';
5import { MapDiscussion, MapDiscussions, MapLeaderboard, MapSummary, Map } from '../types/Map'; 4import { Game } from '../types/Game';
5import { Map, MapDiscussion, MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map';
6import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '../types/Content'; 6import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '../types/Content';
7import { Search } from '../types/Search'; 7import { Search } from '../types/Search';
8import { UserProfile } from '../types/Profile'; 8import { UserProfile } from '../types/Profile';
@@ -15,11 +15,13 @@ export const API = {
15 15
16 get_user: (user_id: string) => get_user(user_id), 16 get_user: (user_id: string) => get_user(user_id),
17 get_games: () => get_games(), 17 get_games: () => get_games(),
18
18 get_chapters: (chapter_id: string) => get_chapters(chapter_id), 19 get_chapters: (chapter_id: string) => get_chapters(chapter_id),
19 get_games_chapters: (game_id: string) => get_games_chapters(game_id), 20 get_games_chapters: (game_id: string) => get_games_chapters(game_id),
20 get_games_maps: (game_id: string) => get_games_maps(game_id), 21 get_game_maps: (game_id: string) => get_game_maps(game_id),
21 get_rankings: () => get_rankings(), 22 get_rankings: () => get_rankings(),
22 get_search: (q: string) => get_search(q), 23 get_search: (q: string) => get_search(q),
24
23 get_map_summary: (map_id: string) => get_map_summary(map_id), 25 get_map_summary: (map_id: string) => get_map_summary(map_id),
24 get_map_leaderboard: (map_id: string) => get_map_leaderboard(map_id), 26 get_map_leaderboard: (map_id: string) => get_map_leaderboard(map_id),
25 get_map_discussions: (map_id: string) => get_map_discussions(map_id), 27 get_map_discussions: (map_id: string) => get_map_discussions(map_id),
@@ -71,10 +73,11 @@ const get_games_chapters = async (game_id: string): Promise<GamesChapters> => {
71 return response.data.data; 73 return response.data.data;
72}; 74};
73 75
74const get_games_maps = async (game_id: string): Promise<Map> => { 76const get_game_maps = async (game_id: string): Promise<Map[]> => {
75 const response = await axios.get(url(`games/${game_id}/maps`)) 77 const response = await axios.get(url(`games/${game_id}/maps`))
76 return response.data.data; 78 return response.data.data.maps;
77} 79};
80
78 81
79// RANKINGS 82// RANKINGS
80const get_rankings = async (): Promise<Ranking> => { 83const get_rankings = async (): Promise<Ranking> => {
diff --git a/frontend/src/components/Loading.tsx b/frontend/src/components/Loading.tsx
new file mode 100644
index 0000000..b7973f1
--- /dev/null
+++ b/frontend/src/components/Loading.tsx
@@ -0,0 +1,11 @@
1import React from 'react';
2
3const Loading: React.FC = () => {
4
5 return (
6 <section id='section6' className='summary2' />
7 );
8
9};
10
11export default Loading;
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx
index adfa718..367dc72 100644
--- a/frontend/src/components/Login.tsx
+++ b/frontend/src/components/Login.tsx
@@ -35,7 +35,7 @@ const Login: React.FC<LoginProps> = ({ token, setToken, profile, setProfile }) =
35 <img src={profile.avatar_link} alt="" /> 35 <img src={profile.avatar_link} alt="" />
36 <span>{profile.user_name}</span> 36 <span>{profile.user_name}</span>
37 </button> 37 </button>
38 <button className='sidebar-button' onClick={_logout}> 38 <button className='logout-button' onClick={_logout}>
39 <img src={ExitIcon} alt="" /><span></span> 39 <img src={ExitIcon} alt="" /><span></span>
40 </button> 40 </button>
41 </Link> 41 </Link>
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index d303927..22d5c8b 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -1,7 +1,7 @@
1import React from 'react'; 1import React from 'react';
2import { Link, useLocation } from 'react-router-dom'; 2import { Link, useLocation } from 'react-router-dom';
3 3
4import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, NewsIcon, PortalIcon, SearchIcon, TableIcon } from '../images/Images'; 4import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '../images/Images';
5import Login from './Login'; 5import Login from './Login';
6import { UserProfile } from '../types/Profile'; 6import { UserProfile } from '../types/Profile';
7import { Search } from '../types/Search'; 7import { Search } from '../types/Search';
@@ -12,9 +12,10 @@ interface SidebarProps {
12 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 12 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
13 profile?: UserProfile; 13 profile?: UserProfile;
14 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 14 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
15 onUploadRun: () => void;
15}; 16};
16 17
17const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile }) => { 18const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUploadRun }) => {
18 19
19 const [searchData, setSearchData] = React.useState<Search | undefined>(undefined); 20 const [searchData, setSearchData] = React.useState<Search | undefined>(undefined);
20 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); 21 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false);
@@ -40,11 +41,19 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile }) => {
40 const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement> 41 const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement>
41 const side = document.querySelector("#sidebar-list") as HTMLElement; 42 const side = document.querySelector("#sidebar-list") as HTMLElement;
42 const searchbar = document.querySelector("#searchbar") as HTMLInputElement; 43 const searchbar = document.querySelector("#searchbar") as HTMLInputElement;
44 const uploadRunBtn = document.querySelector("#upload-run") as HTMLInputElement;
45 const uploadRunSpan = document.querySelector("#upload-run>span") as HTMLInputElement;
43 46
44 if (isSidebarOpen) { 47 if (isSidebarOpen) {
45 if (profile) { 48 if (profile) {
46 const login = document.querySelectorAll(".login>button")[1] as HTMLElement; 49 const login = document.querySelectorAll(".login>button")[1] as HTMLElement;
47 login.style.opacity = "1" 50 login.style.opacity = "1"
51 uploadRunBtn.style.width = "310px"
52 uploadRunBtn.style.padding = "0.4em 0 0 11px"
53 uploadRunSpan.style.opacity = "0"
54 setTimeout(() => {
55 uploadRunSpan.style.opacity = "1"
56 }, 100)
48 } 57 }
49 setSidebarOpen(false); 58 setSidebarOpen(false);
50 side.style.width = "320px" 59 side.style.width = "320px"
@@ -54,14 +63,17 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile }) => {
54 setTimeout(() => { 63 setTimeout(() => {
55 span[i].style.opacity = "1" 64 span[i].style.opacity = "1"
56 }, 100) 65 }, 100)
57 }) 66 });
58 side.style.zIndex = "2" 67 side.style.zIndex = "2"
59 } else { 68 } else {
60 if (profile) { 69 if (profile) {
61 const login = document.querySelectorAll(".login>button")[1] as HTMLElement; 70 const login = document.querySelectorAll(".login>button")[1] as HTMLElement;
62 login.style.opacity = "0" 71 login.style.opacity = "0"
72 uploadRunBtn.style.width = "40px"
73 uploadRunBtn.style.padding = "0.4em 0 0 5px"
74 uploadRunSpan.style.opacity = "0"
63 } 75 }
64 setSidebarOpen(true) 76 setSidebarOpen(true);
65 side.style.width = "40px"; 77 side.style.width = "40px";
66 searchbar.focus(); 78 searchbar.focus();
67 btn.forEach((e, i) => { 79 btn.forEach((e, i) => {
@@ -106,7 +118,7 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile }) => {
106 <img src={LogoIcon} alt="" height={"80px"} /> 118 <img src={LogoIcon} alt="" height={"80px"} />
107 <div id='logo-text'> 119 <div id='logo-text'>
108 <span><b>PORTAL 2</b></span><br /> 120 <span><b>PORTAL 2</b></span><br />
109 <span>Least Portals</span> 121 <span>Least Portals Hub</span>
110 </div> 122 </div>
111 </div> 123 </div>
112 </Link> 124 </Link>
@@ -140,14 +152,21 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile }) => {
140 <div id='sidebar-bottomlist'> 152 <div id='sidebar-bottomlist'>
141 <span></span> 153 <span></span>
142 154
155 {
156 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>
158 :
159 <span></span>
160 }
161
143 <Login setToken={setToken} profile={profile} setProfile={setProfile} /> 162 <Login setToken={setToken} profile={profile} setProfile={setProfile} />
144 163
145 <Link to="/rules" tabIndex={-1}> 164 <Link to="/rules" tabIndex={-1}>
146 <button className='sidebar-button'><img src={BookIcon} alt="leaderboardrules" /><span>Leaderboard&nbsp;Rules</span></button> 165 <button className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard&nbsp;Rules</span></button>
147 </Link> 166 </Link>
148 167
149 <Link to="/about" tabIndex={-1}> 168 <Link to="/about" tabIndex={-1}>
150 <button className='sidebar-button'><img src={HelpIcon} alt="aboutp2lp" /><span>About&nbsp;P2LP</span></button> 169 <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About&nbsp;LPHUB</span></button>
151 </Link> 170 </Link>
152 </div> 171 </div>
153 </div> 172 </div>
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx
new file mode 100644
index 0000000..e099a40
--- /dev/null
+++ b/frontend/src/components/UploadRunDialog.tsx
@@ -0,0 +1,103 @@
1import React from 'react';
2import { UploadRunContent } from '../types/Content';
3
4import '../css/UploadRunDialog.css';
5import { Game } from '../types/Game';
6import Games from '../pages/Games';
7import { Map } from '../types/Map';
8import { API } from '../api/Api';
9
10interface UploadRunDialogProps {
11 open: boolean;
12 onClose: () => void;
13 mapID?: number;
14 games: Game[];
15}
16
17const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ open, onClose, mapID, games }) => {
18
19 const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({
20 map_id: 0,
21 host_demo: null,
22 partner_demo: null,
23 partner_id: undefined,
24 is_partner_orange: undefined,
25 });
26
27 const [selectedGameID, setSelectedGameID] = React.useState<number>(0);
28 const [selectedGameMaps, setSelectedGameMaps] = React.useState<Map[]>([]);
29
30 const [loading, setLoading] = React.useState<boolean>(false);
31
32 const _handle_game_select = async (game_id: string) => {
33 setLoading(true);
34 const gameMaps = await API.get_game_maps(game_id);
35 setSelectedGameMaps(gameMaps);
36 setUploadRunContent({
37 ...uploadRunContent,
38 map_id: gameMaps[0].id,
39 });
40 setSelectedGameID(parseInt(game_id) - 1);
41 setLoading(false);
42 };
43
44 React.useEffect(() => {
45 _handle_game_select("1"); // a different approach?
46 }, []);
47
48 if (open) {
49 return (
50 <>
51 <div id="upload-run-block" />
52 <div id='upload-run-menu'>
53 <div id='upload-run-menu-add'>
54 <div id='upload-run-route-category'>
55 <span>Select Game</span>
56 <select onChange={(e) => _handle_game_select(e.target.value)}>
57 {games.map((game) => (
58 <option key={game.id} value={game.id}>{game.name}</option>
59 ))}
60 </select>
61 {
62 !loading &&
63 (
64 <>
65 <span>Select Map</span>
66 <select onChange={(e) => setUploadRunContent({
67 ...uploadRunContent,
68 map_id: parseInt(e.target.value),
69 })}>
70 {selectedGameMaps && selectedGameMaps.map((gameMap) => (
71 <option key={gameMap.id} value={gameMap.id}>{gameMap.name}</option>
72 ))}
73 </select>
74 <span>Host Demo</span>
75 <input type="file" name="host_demo" id="host_demo" accept=".dem" />
76 {
77 games[selectedGameID].is_coop &&
78 (
79 <>
80 <span>Partner Demo</span>
81 <input type="file" name="partner_demo" id="partner_demo" accept=".dem" />
82 </>
83 )
84 }
85 <button onClick={() => onClose()}>Submit</button>
86 <button onClick={() => onClose()}>Cancel</button>
87 </>
88 )
89 }
90 </div>
91 </div>
92 </div>
93 </>
94 );
95 }
96
97 return (
98 <></>
99 );
100
101};
102
103export default UploadRunDialog; \ No newline at end of file
diff --git a/frontend/src/css/About.css b/frontend/src/css/About.css
new file mode 100644
index 0000000..f998699
--- /dev/null
+++ b/frontend/src/css/About.css
@@ -0,0 +1,16 @@
1#about {
2 overflow: auto;
3 overflow-x: hidden;
4 position: relative;
5
6 width: calc(100% - 380px);
7 height: 100vh;
8 left: 350px;
9
10 padding-right: 30px;
11
12 font-size: 24px;
13 font-family: BarlowSemiCondensed-Regular;
14 color: #cdcfdf;
15
16} \ No newline at end of file
diff --git a/frontend/src/css/Maplist.css b/frontend/src/css/Maplist.css
index bd1f646..230ac98 100644
--- a/frontend/src/css/Maplist.css
+++ b/frontend/src/css/Maplist.css
@@ -107,7 +107,7 @@ h3 {
107 107
108.difficulty-bar { 108.difficulty-bar {
109 display: flex; 109 display: flex;
110 margin: 0px 10px; 110 margin: 15px 10px;
111} 111}
112 112
113.difficulty-bar span { 113.difficulty-bar span {
diff --git a/frontend/src/css/Maps.css b/frontend/src/css/Maps.css
index 335e0b2..d38eea5 100644
--- a/frontend/src/css/Maps.css
+++ b/frontend/src/css/Maps.css
@@ -299,7 +299,7 @@ p>span.portal-count{font-weight: bold;font-size: 100px;vertical-align: -15%;}
299 299
300 300
301#description>iframe{ 301#description>iframe{
302 margin: 4px; 302 margin: 20px;
303 float:right; 303 float:right;
304 border: 0; 304 border: 0;
305 border-radius: 24px; 305 border-radius: 24px;
@@ -311,6 +311,7 @@ p>span.portal-count{font-weight: bold;font-size: 100px;vertical-align: -15%;}
311 display: block; 311 display: block;
312 font-size: 21px; 312 font-size: 21px;
313 word-wrap: break-word; 313 word-wrap: break-word;
314 text-align: justify;
314} 315}
315#description-text>b{font-size: inherit;} 316#description-text>b{font-size: inherit;}
316#description-text>a{font-size: inherit;color: #3c91e6;} 317#description-text>a{font-size: inherit;color: #3c91e6;}
diff --git a/frontend/src/css/Sidebar.css b/frontend/src/css/Sidebar.css
index 34ede80..396f6ac 100644
--- a/frontend/src/css/Sidebar.css
+++ b/frontend/src/css/Sidebar.css
@@ -10,22 +10,22 @@
10 /* logo */ 10 /* logo */
11#logo{ 11#logo{
12 display: grid; 12 display: grid;
13 grid-template-columns: 60px 200px; 13 grid-template-columns: 60px 220px;
14 14
15 15
16 height: 80px; 16 height: 80px;
17 padding: 20px 0 20px 30px; 17 padding: 20px 0 20px 20px;
18 cursor: pointer; 18 cursor: pointer;
19 user-select: none; 19 user-select: none;
20} 20}
21 21
22#logo-text{ 22#logo-text{
23 font-family: BarlowCondensed-Regular; 23 font-family: BarlowCondensed-Regular;
24 font-size: 42px; 24 font-size: 36px;
25 color: #FFF; 25 color: #FFF;
26 line-height: 38px; 26 line-height: 38px;
27} 27}
28span>b{ 28#logo-text>span>b{
29 font-family: BarlowCondensed-Bold; 29 font-family: BarlowCondensed-Bold;
30 font-size: 56px; 30 font-size: 56px;
31} 31}
@@ -55,7 +55,7 @@ span>b{
55 55
56 margin: 0 5px 0 5px; 56 margin: 0 5px 0 5px;
57 justify-items: left; 57 justify-items: left;
58 grid-template-rows: calc(100vh - 670px) 50px 50px 50px; 58 grid-template-rows: calc(100vh - 720px) 50px 50px 50px;
59} 59}
60.sidebar-button>span{ 60.sidebar-button>span{
61 font-family: BarlowSemiCondensed-Regular; 61 font-family: BarlowSemiCondensed-Regular;
@@ -85,6 +85,61 @@ span>b{
85 padding .3s; 85 padding .3s;
86} 86}
87 87
88.logout-button{
89 display: grid;
90 grid-template-columns: 50px auto;
91 place-items: left;
92 text-align: left;
93
94 background-color: inherit;
95 cursor: pointer;
96 border: none;
97 width: 310px;
98 height: 40px;
99 border-radius: 20px;
100 padding: 0.4em 0 0 11px;
101
102 transition:
103 width .3s,
104 background-color .15s,
105 padding .3s;
106}
107
108.submit-run-button {
109 display: grid;
110 grid-template-columns: 50px auto;
111 place-items: left;
112 text-align: left;
113
114 background-color: inherit;
115 cursor: pointer;
116 border: none;
117 width: 310px;
118 height: 40px;
119 border-radius: 20px;
120 padding: 0.4em 0 0 11px;
121
122 transition:
123 width .3s,
124 background-color .15s,
125 padding .3s;
126 background-color: #202232aa;
127}
128
129.submit-run-button:hover {
130 background-color: #202232;
131}
132
133
134.submit-run-button>span{
135 font-family: BarlowSemiCondensed-Regular;
136 font-size: 18px;
137 color: #CDCFDF;
138 height: 32px;
139 line-height: 28px;
140 transition: opacity .1s;
141}
142
88.sidebar-button-selected { 143.sidebar-button-selected {
89 background-color: #202232; 144 background-color: #202232;
90} 145}
diff --git a/frontend/src/css/UploadRunDialog.css b/frontend/src/css/UploadRunDialog.css
new file mode 100644
index 0000000..60f0a28
--- /dev/null
+++ b/frontend/src/css/UploadRunDialog.css
@@ -0,0 +1,47 @@
1div#upload-run{
2 position: absolute;
3 left: 50%;
4 z-index: 20;
5 width: 320px; height: auto;
6 /* background-color: red; */
7
8 transform: translateY(-68%);
9}
10
11
12#upload-run-menu{
13 position: absolute;
14 left: calc(50% + 160px); top: 130px;
15 transform: translateX(-50%);
16 background-color: #2b2e46;
17 z-index: 2; color: white;
18}
19
20#upload-run-menu-add,
21#upload-run-menu-edit{
22 box-shadow: 0 0 40px 16px black;
23 outline: 8px solid #2b2e46;
24 border-radius: 20px;
25 font-size: 40px;
26 display: grid;
27 grid-template-columns: 20% 20% 20% 20% 20%;
28}
29
30#upload-run-menu-add>div,
31#upload-run-menu-edit>div{
32 display: grid;
33 margin: 20px;
34 width: 200px;
35 font-size: 20px;
36}
37
38#upload-run-block{
39 position: absolute;
40 background-color: black;
41 opacity: .3;
42 left: 320px;
43 width: calc(100% - 320px);
44 height: 100%;
45 z-index: 2;
46 cursor: no-drop;
47}
diff --git a/frontend/src/images/Images.tsx b/frontend/src/images/Images.tsx
index d2f6dfb..89b99c0 100644
--- a/frontend/src/images/Images.tsx
+++ b/frontend/src/images/Images.tsx
@@ -19,6 +19,7 @@ import img16 from './png/16.png';
19import img17 from './png/17.png'; 19import img17 from './png/17.png';
20import img18 from './png/18.png'; 20import img18 from './png/18.png';
21import img19 from './png/19.png'; 21import img19 from './png/19.png';
22import img20 from './png/20.png';
22 23
23export const LogoIcon = logo; 24export const LogoIcon = logo;
24export const LoginIcon = login; 25export const LoginIcon = login;
@@ -41,4 +42,5 @@ export const TwitchIcon = img15;
41export const YouTubeIcon = img16; 42export const YouTubeIcon = img16;
42export const SteamIcon = img17; 43export const SteamIcon = img17;
43export const HistoryIcon = img18; 44export const HistoryIcon = img18;
44export const SortIcon = img19; \ No newline at end of file 45export const SortIcon = img19;
46export const UploadIcon = img20; \ No newline at end of file
diff --git a/frontend/src/images/png/20.png b/frontend/src/images/png/20.png
new file mode 100644
index 0000000..1af0a97
--- /dev/null
+++ b/frontend/src/images/png/20.png
Binary files differ
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx
new file mode 100644
index 0000000..886808b
--- /dev/null
+++ b/frontend/src/pages/About.tsx
@@ -0,0 +1,36 @@
1import React from 'react';
2import ReactMarkdown from 'react-markdown';
3
4import '../css/About.css';
5
6const About: React.FC = () => {
7
8 const [aboutText, setAboutText] = React.useState<string>("");
9
10 React.useEffect(() => {
11 const fetchReadme = async () => {
12 try {
13 const response = await fetch(
14 'https://raw.githubusercontent.com/pektezol/leastportalshub/main/README.md'
15 );
16 if (!response.ok) {
17 throw new Error('Failed to fetch README');
18 }
19 const readmeText = await response.text();
20 setAboutText(readmeText);
21 } catch (error) {
22 console.error('Error fetching README:', error);
23 }
24 };
25 fetchReadme();
26 }, []);
27
28
29 return (
30 <div id="about">
31 <ReactMarkdown>{aboutText}</ReactMarkdown>
32 </div>
33 );
34};
35
36export default About;
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx
index eb7177f..ea136c2 100644
--- a/frontend/src/pages/Games.tsx
+++ b/frontend/src/pages/Games.tsx
@@ -2,16 +2,13 @@ import React from 'react';
2 2
3import GameEntry from '../components/GameEntry'; 3import GameEntry from '../components/GameEntry';
4import { Game } from '../types/Game'; 4import { Game } from '../types/Game';
5import { API } from '../api/Api';
6import "../css/Maps.css" 5import "../css/Maps.css"
7 6
8const Games: React.FC = () => { 7interface GamesProps {
9 const [games, setGames] = React.useState<Game[]>([]); 8 games: Game[];
9}
10 10
11 const _fetch_games = async () => { 11const Games: React.FC<GamesProps> = ({ games }) => {
12 const games = await API.get_games();
13 setGames(games);
14 };
15 12
16 const _page_load = () => { 13 const _page_load = () => {
17 const loaders = document.querySelectorAll(".loader"); 14 const loaders = document.querySelectorAll(".loader");
@@ -21,7 +18,9 @@ const Games: React.FC = () => {
21 } 18 }
22 19
23 React.useEffect(() => { 20 React.useEffect(() => {
24 _fetch_games(); 21 document.querySelectorAll(".games-page-item-body").forEach((game, index) => {
22 game.innerHTML = "";
23 });
25 _page_load(); 24 _page_load();
26 }, []); 25 }, []);
27 26
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx
index 62a8f1c..d4f3095 100644
--- a/frontend/src/pages/Homepage.tsx
+++ b/frontend/src/pages/Homepage.tsx
@@ -6,6 +6,7 @@ const Homepage: React.FC = () => {
6 return ( 6 return (
7 <main> 7 <main>
8 <section> 8 <section>
9 <p/>
9 <h1><img src={PortalIcon} alt="lphub"/>Welcome to Least Portals Hub!</h1> 10 <h1><img src={PortalIcon} alt="lphub"/>Welcome to Least Portals Hub!</h1>
10 <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>
11 <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>
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx
index 3bc5f74..5d0c852 100644
--- a/frontend/src/pages/Maplist.tsx
+++ b/frontend/src/pages/Maplist.tsx
@@ -73,14 +73,9 @@ const Maplist: React.FC = () => {
73 setNumChapters(games_chapters.chapters.length); 73 setNumChapters(games_chapters.chapters.length);
74 } 74 }
75 75
76 const _fetch_maps = async () => {
77 const maps = await API.get_games_maps(gameId.toString());
78 setLoad(true);
79 }
80
81 _fetch_game(); 76 _fetch_game();
82 _fetch_game_chapters(); 77 _fetch_game_chapters();
83 _fetch_maps(); 78 setLoad(true);
84 }, []); 79 }, []);
85 80
86 useEffect(() => { 81 useEffect(() => {
@@ -97,7 +92,7 @@ const Maplist: React.FC = () => {
97 <Link to="/games"> 92 <Link to="/games">
98 <button className="nav-button" style={{ borderRadius: "20px" }}> 93 <button className="nav-button" style={{ borderRadius: "20px" }}>
99 <i className="triangle"></i> 94 <i className="triangle"></i>
100 <span>Games list</span> 95 <span>Games List</span>
101 </button> 96 </button>
102 </Link> 97 </Link>
103 </section> 98 </section>
@@ -162,7 +157,7 @@ const Maplist: React.FC = () => {
162 </div> 157 </div>
163 </div> 158 </div>
164 <div className="difficulty-bar"> 159 <div className="difficulty-bar">
165 <span>Difficulty:</span> 160 {/* <span>Difficulty:</span> */}
166 <div className={map.difficulty == 0 ? "one" : map.difficulty == 1 ? "two" : map.difficulty == 2 ? "three" : map.difficulty == 3 ? "four" : map.difficulty == 4 ? "five" : "one"}> 161 <div className={map.difficulty == 0 ? "one" : map.difficulty == 1 ? "two" : map.difficulty == 2 ? "three" : map.difficulty == 3 ? "four" : map.difficulty == 4 ? "five" : "one"}>
167 <div className="difficulty-point"></div> 162 <div className="difficulty-point"></div>
168 <div className="difficulty-point"></div> 163 <div className="difficulty-point"></div>
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx
index 707d865..5548650 100644
--- a/frontend/src/pages/Maps.tsx
+++ b/frontend/src/pages/Maps.tsx
@@ -7,21 +7,24 @@ 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';
10import { API } from '../api/Api'; 11import { API } from '../api/Api';
11import "../css/Maps.css"; 12import "../css/Maps.css";
13import Loading from '../components/Loading';
12 14
13interface MapProps { 15interface MapProps {
16 profile?: UserProfile;
14 isModerator: boolean; 17 isModerator: boolean;
18 onUploadRun: (mapID: number) => void;
15}; 19};
16 20
17const Maps: React.FC<MapProps> = ({ isModerator }) => { 21const Maps: React.FC<MapProps> = ({ profile, isModerator, onUploadRun }) => {
18 22
19 const [selectedRun, setSelectedRun] = React.useState<number>(0); 23 const [selectedRun, setSelectedRun] = React.useState<number>(0);
20 24
21 const [mapSummaryData, setMapSummaryData] = React.useState<MapSummary | undefined>(undefined); 25 const [mapSummaryData, setMapSummaryData] = React.useState<MapSummary | undefined>(undefined);
22 const [mapLeaderboardData, setMapLeaderboardData] = React.useState<MapLeaderboard | undefined>(undefined); 26 const [mapLeaderboardData, setMapLeaderboardData] = React.useState<MapLeaderboard | undefined>(undefined);
23 const [mapDiscussionsData, setMapDiscussionsData] = React.useState<MapDiscussions | undefined>(undefined) 27 const [mapDiscussionsData, setMapDiscussionsData] = React.useState<MapDiscussions | undefined>(undefined);
24
25 28
26 const [navState, setNavState] = React.useState<number>(0); 29 const [navState, setNavState] = React.useState<number>(0);
27 30
@@ -51,8 +54,23 @@ const Maps: React.FC<MapProps> = ({ isModerator }) => {
51 }, []); 54 }, []);
52 55
53 if (!mapSummaryData) { 56 if (!mapSummaryData) {
57 // loading placeholder
54 return ( 58 return (
55 <></> 59 <main>
60 <section id='section1' className='summary1'>
61 <div>
62 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 20px 20px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link>
63 </div>
64 </section>
65
66 <section id='section2' className='summary1'>
67 <button className='nav-button'><img src={PortalIcon} alt="" /><span>Summary</span></button>
68 <button className='nav-button'><img src={FlagIcon} alt="" /><span>Leaderboards</span></button>
69 <button className='nav-button'><img src={ChatIcon} alt="" /><span>Discussions</span></button>
70 </section>
71
72 <Loading />
73 </main>
56 ); 74 );
57 } 75 }
58 76
@@ -66,12 +84,11 @@ const Maps: React.FC<MapProps> = ({ isModerator }) => {
66 <main> 84 <main>
67 <section id='section1' className='summary1'> 85 <section id='section1' className='summary1'>
68 <div> 86 <div>
69 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 0px 0px 20px" }}><i className='triangle'></i><span>Games list</span></button></Link> 87 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 0px 0px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link>
70 <Link to={`/games/${!mapSummaryData.map.is_coop ? "1" : "2"}?chapter=${mapSummaryData.map.chapter_name}`}><button className='nav-button' style={{ borderRadius: "0px 20px 20px 0px", marginLeft: "2px" }}><i className='triangle'></i><span>{mapSummaryData.map.chapter_name}</span></button></Link> 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>
71 <br /><span><b>{mapSummaryData.map.map_name}</b></span> 89 <br /><span><b>{mapSummaryData.map.map_name}</b></span>
90 {profile && <button onClick={() => onUploadRun(mapSummaryData.map.id)}>Submit a Run</button>}
72 </div> 91 </div>
73
74
75 </section> 92 </section>
76 93
77 <section id='section2' className='summary1'> 94 <section id='section2' className='summary1'>
diff --git a/frontend/src/types/Content.tsx b/frontend/src/types/Content.tsx
index e593505..5733f1f 100644
--- a/frontend/src/types/Content.tsx
+++ b/frontend/src/types/Content.tsx
@@ -16,3 +16,11 @@ export interface MapDiscussionContent {
16export interface MapDiscussionCommentContent { 16export interface MapDiscussionCommentContent {
17 comment: string; 17 comment: string;
18}; 18};
19
20export interface UploadRunContent {
21 map_id: number;
22 host_demo: File | null;
23 partner_demo: File | null;
24 partner_id?: string;
25 is_partner_orange?: boolean;
26};