aboutsummaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/App.tsx3
-rw-r--r--frontend/src/api/Api.tsx23
-rw-r--r--frontend/src/components/MapEntry.tsx12
-rw-r--r--frontend/src/css/Maplist.css198
-rw-r--r--frontend/src/css/Maps.css2
-rw-r--r--frontend/src/pages/Games.tsx6
-rw-r--r--frontend/src/pages/Maplist.tsx183
-rw-r--r--frontend/src/types/Chapters.tsx19
8 files changed, 437 insertions, 9 deletions
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b9e84f4..fdf1077 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -10,7 +10,7 @@ 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';
13 13import Maplist from './pages/Maplist';
14 14
15const App: React.FC = () => { 15const App: React.FC = () => {
16 const [token, setToken] = React.useState<string | undefined>(undefined); 16 const [token, setToken] = React.useState<string | undefined>(undefined);
@@ -32,6 +32,7 @@ const App: React.FC = () => {
32 <Route path="/users/*" element={<User />} /> 32 <Route path="/users/*" element={<User />} />
33 <Route path="/games" element={<Games />} /> 33 <Route path="/games" element={<Games />} />
34 <Route path="/maps/*" element={<Maps isModerator={isModerator} />} /> 34 <Route path="/maps/*" element={<Maps isModerator={isModerator} />} />
35 <Route path='/games/:id' element={<Maplist></Maplist>}></Route>
35 <Route path="*" element={"404"} /> 36 <Route path="*" element={"404"} />
36 </Routes> 37 </Routes>
37 </> 38 </>
diff --git a/frontend/src/api/Api.tsx b/frontend/src/api/Api.tsx
index 9e45bc4..326052f 100644
--- a/frontend/src/api/Api.tsx
+++ b/frontend/src/api/Api.tsx
@@ -1,7 +1,8 @@
1import axios from 'axios'; 1import axios from 'axios';
2 2
3import { Game } from '../types/Game'; 3import { Game, GameChapters } from '../types/Game';
4import { MapDiscussion, MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map'; 4import { GameChapter, GamesChapters } from '../types/Chapters';
5import { MapDiscussion, MapDiscussions, MapLeaderboard, MapSummary, Map } from '../types/Map';
5import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '../types/Content'; 6import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '../types/Content';
6import { Search } from '../types/Search'; 7import { Search } from '../types/Search';
7import { UserProfile } from '../types/Profile'; 8import { UserProfile } from '../types/Profile';
@@ -13,6 +14,9 @@ export const API = {
13 14
14 get_user: (user_id: string) => get_user(user_id), 15 get_user: (user_id: string) => get_user(user_id),
15 get_games: () => get_games(), 16 get_games: () => get_games(),
17 get_chapters: (chapter_id: string) => get_chapters(chapter_id),
18 get_games_chapters: (game_id: string) => get_games_chapters(game_id),
19 get_games_maps: (game_id: string) => get_games_maps(game_id),
16 get_search: (q: string) => get_search(q), 20 get_search: (q: string) => get_search(q),
17 get_map_summary: (map_id: string) => get_map_summary(map_id), 21 get_map_summary: (map_id: string) => get_map_summary(map_id),
18 get_map_leaderboard: (map_id: string) => get_map_leaderboard(map_id), 22 get_map_leaderboard: (map_id: string) => get_map_leaderboard(map_id),
@@ -55,6 +59,21 @@ const get_games = async (): Promise<Game[]> => {
55 return response.data.data; 59 return response.data.data;
56}; 60};
57 61
62const get_chapters = async (chapter_id: string): Promise<GameChapter> => {
63 const response = await axios.get(url(`chapters/${chapter_id}`));
64 return response.data.data;
65}
66
67const get_games_chapters = async (game_id: string): Promise<GamesChapters> => {
68 const response = await axios.get(url(`games/${game_id}`));
69 return response.data.data;
70};
71
72const get_games_maps = async (game_id: string): Promise<Map> => {
73 const response = await axios.get(url(`games/${game_id}/maps`))
74 return response.data.data;
75}
76
58// SEARCH 77// SEARCH
59 78
60const get_search = async (q: string): Promise<Search> => { 79const get_search = async (q: string): Promise<Search> => {
diff --git a/frontend/src/components/MapEntry.tsx b/frontend/src/components/MapEntry.tsx
new file mode 100644
index 0000000..0f494ad
--- /dev/null
+++ b/frontend/src/components/MapEntry.tsx
@@ -0,0 +1,12 @@
1import React from 'react';
2import { Link } from "react-router-dom";
3
4const MapEntry: React.FC = () => {
5 return (
6 <div>
7
8 </div>
9 )
10}
11
12export default MapEntry;
diff --git a/frontend/src/css/Maplist.css b/frontend/src/css/Maplist.css
new file mode 100644
index 0000000..bd1f646
--- /dev/null
+++ b/frontend/src/css/Maplist.css
@@ -0,0 +1,198 @@
1h1 {
2 font-family: "BarlowCondensed-Bold";
3 margin: 0px 0px;
4}
5
6h2 {
7 margin: 20px 0px;
8 font-family: "BarlowSemiCondensed-SemiBold";
9 font-size: 96px;
10}
11
12h3 {
13 font-family: "BarlowSemiCondensed-Regular";
14 margin: 0px 10px;
15 font-size: 42px;
16}
17
18.game-header {
19 text-align: center;
20 border-radius: 24px;
21 overflow: hidden;
22 background-size: cover;
23 background-position: 25%;
24 margin-top: 12px;
25}
26
27.blur {
28 backdrop-filter: blur(4px);
29 display: flex;
30 flex-direction: column;
31 width: 100%;
32}
33
34.blur.map {
35 flex-direction: row;
36 align-items: center;
37 justify-content: center;
38}
39
40.blur.map span {
41 width: fit-content;
42}
43
44.blur.map span:nth-child(1) {
45 font-family: "BarlowSemiCondensed-SemiBold";
46 font-size: 60px;
47 margin-right: 6px;
48}
49
50.game-header-portal-count {
51 height: 100%;
52 display: flex;
53 justify-content: center;
54 align-items: center;
55}
56
57.game-header-categories {
58 display: flex;
59 height: 50px;
60 background-color: #202232;
61 gap: 2px;
62}
63
64.game-cat-button {
65 background-color: #2b2e46;
66 border: 0;
67 color: #cdcfdf;
68 font-family: "BarlowSemiCondensed-Regular";
69 font-size: 22px;
70 cursor: pointer;
71 transition: all 0.1s;
72 width: 100%;
73}
74
75.game-cat-button:hover, .game-cat-button.selected {
76 background-color: #202232;
77}
78
79/* maplist */
80.maplist {
81 display: grid;
82 grid-template-columns: repeat(4, 1fr);
83 grid-gap: 20px;
84 margin: 20px 0px;
85}
86
87.maplist-entry {
88 background-color: #202232;
89 border-radius: 24px;
90 overflow: hidden;
91}
92
93.maplist-entry span {
94 text-align: center;
95 font-size: 20px;
96 width: 100%;
97 display: block;
98 margin: 5px 0px;
99 color: #cdcfdf;
100}
101
102.map-entry-image {
103 display: flex;
104 height: 150px;
105 background-size: cover;
106}
107
108.difficulty-bar {
109 display: flex;
110 margin: 0px 10px;
111}
112
113.difficulty-bar span {
114 text-align: left;
115 margin-left: 0px;
116 width: fit-content;
117}
118
119.difficulty-bar div {
120 display: flex;
121 width: 100%;
122 align-items: center;
123 justify-content: center;
124 gap: 5px;
125 border-radius: 2000px;
126 margin-left: 2px;
127 transform: translateY(1px);
128}
129
130.difficulty-bar div div {
131 display: flex;
132 height: 3px;
133 width: 100%;
134 background-color: #2B2E46;
135}
136
137.difficulty-bar div.one .difficulty-point:nth-child(1) {
138 background-color: #51C355;
139}
140
141.difficulty-bar div.two .difficulty-point:nth-child(-n+2) {
142 background-color: #8AC93A;
143}
144
145.difficulty-bar div.three .difficulty-point:nth-child(-n+3) {
146 background-color: #8AC93A;
147}
148
149.difficulty-bar div.four .difficulty-point:nth-child(-n+4) {
150 background-color: #C35F51;
151}
152
153.difficulty-bar div.five .difficulty-point:nth-child(-n+5) {
154 background-color: #dd422e;
155}
156
157.dropdown {
158 cursor: pointer;
159 user-select: none;
160 display: flex;
161 width: fit-content;
162 align-items: center;
163}
164
165.dropdown i {
166 transform: translate(5px, 8px) rotate(-90deg);
167}
168
169.dropdown-elements {
170 position: absolute;
171 z-index: 1000;
172 background-color: #2B2E46;
173 border-radius: 15px;
174 overflow: hidden;
175 padding: 4px 4px;
176 animation: dropdown-in 0.1s ease;
177}
178
179.dropdown-element {
180 cursor: pointer;
181 font-size: 20px;
182 border-radius: 2000px;
183 padding: 1px 4px;
184}
185
186.dropdown-element:hover {
187 background-color: #202232;
188}
189
190@keyframes dropdown-in {
191 0% {
192 opacity: 0;
193 }
194
195 100% {
196 opacity: 1;
197 }
198}
diff --git a/frontend/src/css/Maps.css b/frontend/src/css/Maps.css
index d164d3b..335e0b2 100644
--- a/frontend/src/css/Maps.css
+++ b/frontend/src/css/Maps.css
@@ -29,7 +29,7 @@
29 height: 40px; 29 height: 40px;
30 background-color: #2b2e46; 30 background-color: #2b2e46;
31 31
32 color: inherit; 32 color: #cdcfdf;
33 font-size: 18px; 33 font-size: 18px;
34 font-family: inherit; 34 font-family: inherit;
35 border: none; 35 border: none;
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx
index ce3db76..eb7177f 100644
--- a/frontend/src/pages/Games.tsx
+++ b/frontend/src/pages/Games.tsx
@@ -18,13 +18,9 @@ const Games: React.FC = () => {
18 loaders.forEach((loader) => { 18 loaders.forEach((loader) => {
19 (loader as HTMLElement).style.display = "none"; 19 (loader as HTMLElement).style.display = "none";
20 }); 20 });
21 } 21 }
22 22
23 React.useEffect(() => { 23 React.useEffect(() => {
24 document.querySelectorAll(".games-page-item-body").forEach((game, index) => {
25 game.innerHTML = "";
26 });
27
28 _fetch_games(); 24 _fetch_games();
29 _page_load(); 25 _page_load();
30 }, []); 26 }, []);
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx
new file mode 100644
index 0000000..b1b1664
--- /dev/null
+++ b/frontend/src/pages/Maplist.tsx
@@ -0,0 +1,183 @@
1import React, { useEffect } from "react";
2import { Link, useLocation, useParams } from "react-router-dom";
3
4import "../css/Maplist.css";
5import { API } from "../api/Api";
6import { Game, GameChapters } from "../types/Game";
7import { GameChapter, GamesChapters } from "../types/Chapters";
8import { Map } from "../types/Map";
9
10const Maplist: React.FC = () => {
11 const [game, setGame] = React.useState<Game | null>(null);
12 const [catNum, setCatNum] = React.useState(0);
13 const [id, setId] = React.useState(0);
14 const [category, setCategory] = React.useState(0);
15 const [load, setLoad] = React.useState(false);
16 const [currentlySelected, setCurrentlySelected] = React.useState<number>(0);
17 const [hasClicked, setHasClicked] = React.useState(false);
18 const [gameChapters, setGameChapters] = React.useState<GamesChapters>();
19 const [curChapter, setCurChapter] = React.useState<GameChapter>();
20 const [numChapters, setNumChapters] = React.useState<number>(0);
21
22 const [dropdownActive, setDropdownActive] = React.useState("none");
23
24 const params = useParams<{ id: string }>();
25 const location = useLocation();
26
27 function _update_currently_selected(catNum2: number) {
28 setCurrentlySelected(catNum2);
29 setHasClicked(true);
30 }
31
32 const _fetch_chapters = async (chapter_id: string) => {
33 const chapters = await API.get_chapters(chapter_id);
34 setCurChapter(chapters);
35 }
36
37 const _handle_dropdown_click = () => {
38 if (dropdownActive == "none") {
39 setDropdownActive("block");
40 } else {
41 setDropdownActive("none");
42 }
43 }
44
45 // im sorry but im too lazy to fix this right now
46 useEffect(() => {
47 // gameID
48 const gameId = parseFloat(params.id || "");
49 setId(gameId);
50
51 // location query params
52 const queryParams = new URLSearchParams(location.search);
53 if (queryParams.get("cat")) {
54 const cat = parseFloat(queryParams.get("cat") || "");
55 setCategory(cat);
56 setCatNum(cat - 1);
57 }
58
59 const _fetch_game = async () => {
60 const games = await API.get_games();
61 const foundGame = games.find((game) => game.id === gameId);
62 // console.log(foundGame)
63 if (foundGame) {
64 setGame(foundGame);
65 }
66 };
67
68 const _fetch_game_chapters = async () => {
69 const games_chapters = await API.get_games_chapters(gameId.toString());
70 setGameChapters(games_chapters);
71 setNumChapters(games_chapters.chapters.length);
72 }
73
74 const _fetch_maps = async () => {
75 const maps = await API.get_games_maps(gameId.toString());
76 setLoad(true);
77 }
78
79 _fetch_game();
80 _fetch_game_chapters();
81 _fetch_maps();
82 }, []);
83
84 useEffect(() => {
85 if (gameChapters != undefined) {
86 _fetch_chapters(gameChapters!.chapters[0].id.toString());
87 }
88 }, [gameChapters])
89
90
91
92 return (
93 <main>
94 <section style={{ marginTop: "20px" }}>
95 <Link to="/games">
96 <button className="nav-button" style={{ borderRadius: "20px" }}>
97 <i className="triangle"></i>
98 <span>Games list</span>
99 </button>
100 </Link>
101 </section>
102 {!load ? (
103 <div></div>
104 ) : (
105 <section>
106 <h1>{game?.name}</h1>
107 <div
108 style={{ backgroundImage: `url(${game?.image})` }}
109 className="game-header"
110 >
111 <div className="blur">
112 <div className="game-header-portal-count">
113 <h2>
114 {
115 game?.category_portals.find(
116 (obj) => obj.category.id === catNum + 1
117 )?.portal_count
118 }
119 </h2>
120 <h3>portals</h3>
121 </div>
122 <div className="game-header-categories">
123 {game?.category_portals.map((cat, index) => (
124 <button key={index} className={currentlySelected == cat.category.id || cat.category.id - 1 == catNum && !hasClicked ? "game-cat-button selected" : "game-cat-button"} onClick={() => {setCatNum(cat.category.id - 1); _update_currently_selected(cat.category.id)}}>
125 <span>{cat.category.name}</span>
126 </button>
127 ))}
128 </div>
129 </div>
130 </div>
131
132 <div>
133 <section className="chapter-select-container">
134 <div>
135 <span style={{fontSize: "18px", transform: "translateY(5px)", display: "block", marginTop: "10px"}}>{curChapter?.chapter.name.split(" - ")[0]}</span>
136 </div>
137 <div onClick={_handle_dropdown_click} className="dropdown">
138 <span>{curChapter?.chapter.name.split(" - ")[1]}</span>
139 <i className="triangle"></i>
140 </div>
141 <div className="dropdown-elements" style={{display: dropdownActive}}>
142 {gameChapters?.chapters.map((chapter, i) => {
143 return <div className="dropdown-element" onClick={() => {_fetch_chapters(chapter.id.toString()); _handle_dropdown_click()}}>{chapter.name}</div>
144 })
145
146 }
147 </div>
148 </section>
149 <section className="maplist">
150 {curChapter?.maps.map((map, i) => {
151 return <div className="maplist-entry">
152 <Link to={`/maps/${map.id}`}>
153 <span>{map.name}</span>
154 <div className="map-entry-image" style={{backgroundImage: `url(${map.image})`}}>
155 <div className="blur map">
156 <span>{map.is_disabled ? map.category_portals[0].portal_count : map.category_portals.find(
157 (obj) => obj.category.id === catNum + 1
158 )?.portal_count}</span>
159 <span>portals</span>
160 </div>
161 </div>
162 <div className="difficulty-bar">
163 <span>Difficulty:</span>
164 <div className={map.difficulty == 0 ? "one" : map.difficulty == 1 ? "two" : map.difficulty == 2 ? "three" : map.difficulty == 3 ? "four" : map.difficulty == 4 ? "five" : "one"}>
165 <div className="difficulty-point"></div>
166 <div className="difficulty-point"></div>
167 <div className="difficulty-point"></div>
168 <div className="difficulty-point"></div>
169 <div className="difficulty-point"></div>
170 </div>
171 </div>
172 </Link>
173 </div>
174 })}
175 </section>
176 </div>
177 </section>
178 )}
179 </main>
180 );
181};
182
183export default Maplist;
diff --git a/frontend/src/types/Chapters.tsx b/frontend/src/types/Chapters.tsx
new file mode 100644
index 0000000..2c0afdd
--- /dev/null
+++ b/frontend/src/types/Chapters.tsx
@@ -0,0 +1,19 @@
1import { Game } from "./Game";
2import { Map } from "./Map";
3
4interface Chapter {
5 id: number;
6 name: string;
7 image: string;
8 is_disabled: boolean;
9}
10
11export interface GameChapter {
12 chapter: Chapter;
13 maps: Map[];
14}
15
16export interface GamesChapters {
17 game: Game;
18 chapters: Chapter[];
19} \ No newline at end of file