diff options
| author | Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> | 2024-09-09 19:29:42 +0300 |
|---|---|---|
| committer | Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> | 2024-09-09 19:29:42 +0300 |
| commit | 89560a61bc6e41d86acaea596762eda2da38fe50 (patch) | |
| tree | 1cf4b7c73c17f045d3f4837b480ddf7a61230a94 /frontend | |
| parent | refactor: rankings page (diff) | |
| download | lphub-89560a61bc6e41d86acaea596762eda2da38fe50.tar.gz lphub-89560a61bc6e41d86acaea596762eda2da38fe50.tar.bz2 lphub-89560a61bc6e41d86acaea596762eda2da38fe50.zip | |
refactor: upload run form, lots of random shit
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/src/App.css | 5 | ||||
| -rw-r--r-- | frontend/src/App.tsx | 34 | ||||
| -rw-r--r-- | frontend/src/api/Api.tsx | 15 | ||||
| -rw-r--r-- | frontend/src/components/Loading.tsx | 11 | ||||
| -rw-r--r-- | frontend/src/components/Login.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar.tsx | 33 | ||||
| -rw-r--r-- | frontend/src/components/UploadRunDialog.tsx | 103 | ||||
| -rw-r--r-- | frontend/src/css/About.css | 16 | ||||
| -rw-r--r-- | frontend/src/css/Maplist.css | 2 | ||||
| -rw-r--r-- | frontend/src/css/Maps.css | 3 | ||||
| -rw-r--r-- | frontend/src/css/Sidebar.css | 65 | ||||
| -rw-r--r-- | frontend/src/css/UploadRunDialog.css | 47 | ||||
| -rw-r--r-- | frontend/src/images/Images.tsx | 4 | ||||
| -rw-r--r-- | frontend/src/images/png/20.png | bin | 0 -> 2072 bytes | |||
| -rw-r--r-- | frontend/src/pages/About.tsx | 36 | ||||
| -rw-r--r-- | frontend/src/pages/Games.tsx | 15 | ||||
| -rw-r--r-- | frontend/src/pages/Homepage.tsx | 1 | ||||
| -rw-r--r-- | frontend/src/pages/Maplist.tsx | 11 | ||||
| -rw-r--r-- | frontend/src/pages/Maps.tsx | 33 | ||||
| -rw-r--r-- | frontend/src/types/Content.tsx | 8 |
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 | ||
| 18 | a { | ||
| 19 | color: inherit; | ||
| 20 | width: fit-content; | ||
| 21 | } | ||
| 22 | |||
| 18 | body { | 23 | body { |
| 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'; | |||
| 10 | import Maps from './pages/Maps'; | 10 | import Maps from './pages/Maps'; |
| 11 | import User from './pages/User'; | 11 | import User from './pages/User'; |
| 12 | import Homepage from './pages/Homepage'; | 12 | import Homepage from './pages/Homepage'; |
| 13 | import UploadRunDialog from './components/UploadRunDialog'; | ||
| 14 | import About from './pages/About'; | ||
| 15 | import { Game } from './types/Game'; | ||
| 16 | import { API } from './api/Api'; | ||
| 13 | import Maplist from './pages/Maplist'; | 17 | import Maplist from './pages/Maplist'; |
| 14 | import Rankings from './pages/Rankings'; | 18 | import 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 @@ | |||
| 1 | import axios from 'axios'; | 1 | import axios from 'axios'; |
| 2 | 2 | ||
| 3 | import { Game, GameChapters } from '../types/Game'; | ||
| 4 | import { GameChapter, GamesChapters } from '../types/Chapters'; | 3 | import { GameChapter, GamesChapters } from '../types/Chapters'; |
| 5 | import { MapDiscussion, MapDiscussions, MapLeaderboard, MapSummary, Map } from '../types/Map'; | 4 | import { Game } from '../types/Game'; |
| 5 | import { Map, MapDiscussion, MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map'; | ||
| 6 | import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '../types/Content'; | 6 | import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '../types/Content'; |
| 7 | import { Search } from '../types/Search'; | 7 | import { Search } from '../types/Search'; |
| 8 | import { UserProfile } from '../types/Profile'; | 8 | import { 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 | ||
| 74 | const get_games_maps = async (game_id: string): Promise<Map> => { | 76 | const 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 |
| 80 | const get_rankings = async (): Promise<Ranking> => { | 83 | const 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 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | |||
| 3 | const Loading: React.FC = () => { | ||
| 4 | |||
| 5 | return ( | ||
| 6 | <section id='section6' className='summary2' /> | ||
| 7 | ); | ||
| 8 | |||
| 9 | }; | ||
| 10 | |||
| 11 | export 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 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | import { Link, useLocation } from 'react-router-dom'; | 2 | import { Link, useLocation } from 'react-router-dom'; |
| 3 | 3 | ||
| 4 | import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, NewsIcon, PortalIcon, SearchIcon, TableIcon } from '../images/Images'; | 4 | import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '../images/Images'; |
| 5 | import Login from './Login'; | 5 | import Login from './Login'; |
| 6 | import { UserProfile } from '../types/Profile'; | 6 | import { UserProfile } from '../types/Profile'; |
| 7 | import { Search } from '../types/Search'; | 7 | import { 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 | ||
| 17 | const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile }) => { | 18 | const 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 a 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 Rules</span></button> | 165 | <button className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard 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 P2LP</span></button> | 169 | <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About 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 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import { UploadRunContent } from '../types/Content'; | ||
| 3 | |||
| 4 | import '../css/UploadRunDialog.css'; | ||
| 5 | import { Game } from '../types/Game'; | ||
| 6 | import Games from '../pages/Games'; | ||
| 7 | import { Map } from '../types/Map'; | ||
| 8 | import { API } from '../api/Api'; | ||
| 9 | |||
| 10 | interface UploadRunDialogProps { | ||
| 11 | open: boolean; | ||
| 12 | onClose: () => void; | ||
| 13 | mapID?: number; | ||
| 14 | games: Game[]; | ||
| 15 | } | ||
| 16 | |||
| 17 | const 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 | |||
| 103 | export 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 | } |
| 28 | span>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 @@ | |||
| 1 | div#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'; | |||
| 19 | import img17 from './png/17.png'; | 19 | import img17 from './png/17.png'; |
| 20 | import img18 from './png/18.png'; | 20 | import img18 from './png/18.png'; |
| 21 | import img19 from './png/19.png'; | 21 | import img19 from './png/19.png'; |
| 22 | import img20 from './png/20.png'; | ||
| 22 | 23 | ||
| 23 | export const LogoIcon = logo; | 24 | export const LogoIcon = logo; |
| 24 | export const LoginIcon = login; | 25 | export const LoginIcon = login; |
| @@ -41,4 +42,5 @@ export const TwitchIcon = img15; | |||
| 41 | export const YouTubeIcon = img16; | 42 | export const YouTubeIcon = img16; |
| 42 | export const SteamIcon = img17; | 43 | export const SteamIcon = img17; |
| 43 | export const HistoryIcon = img18; | 44 | export const HistoryIcon = img18; |
| 44 | export const SortIcon = img19; \ No newline at end of file | 45 | export const SortIcon = img19; |
| 46 | export 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 @@ | |||
| 1 | import React from 'react'; | ||
| 2 | import ReactMarkdown from 'react-markdown'; | ||
| 3 | |||
| 4 | import '../css/About.css'; | ||
| 5 | |||
| 6 | const 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 | |||
| 36 | export 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 | ||
| 3 | import GameEntry from '../components/GameEntry'; | 3 | import GameEntry from '../components/GameEntry'; |
| 4 | import { Game } from '../types/Game'; | 4 | import { Game } from '../types/Game'; |
| 5 | import { API } from '../api/Api'; | ||
| 6 | import "../css/Maps.css" | 5 | import "../css/Maps.css" |
| 7 | 6 | ||
| 8 | const Games: React.FC = () => { | 7 | interface GamesProps { |
| 9 | const [games, setGames] = React.useState<Game[]>([]); | 8 | games: Game[]; |
| 9 | } | ||
| 10 | 10 | ||
| 11 | const _fetch_games = async () => { | 11 | const 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'; | |||
| 7 | import Discussions from '../components/Discussions'; | 7 | import Discussions from '../components/Discussions'; |
| 8 | import ModMenu from '../components/ModMenu'; | 8 | import ModMenu from '../components/ModMenu'; |
| 9 | import { MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map'; | 9 | import { MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map'; |
| 10 | import { UserProfile } from '../types/Profile'; | ||
| 10 | import { API } from '../api/Api'; | 11 | import { API } from '../api/Api'; |
| 11 | import "../css/Maps.css"; | 12 | import "../css/Maps.css"; |
| 13 | import Loading from '../components/Loading'; | ||
| 12 | 14 | ||
| 13 | interface MapProps { | 15 | interface MapProps { |
| 16 | profile?: UserProfile; | ||
| 14 | isModerator: boolean; | 17 | isModerator: boolean; |
| 18 | onUploadRun: (mapID: number) => void; | ||
| 15 | }; | 19 | }; |
| 16 | 20 | ||
| 17 | const Maps: React.FC<MapProps> = ({ isModerator }) => { | 21 | const 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 { | |||
| 16 | export interface MapDiscussionCommentContent { | 16 | export interface MapDiscussionCommentContent { |
| 17 | comment: string; | 17 | comment: string; |
| 18 | }; | 18 | }; |
| 19 | |||
| 20 | export 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 | }; | ||