aboutsummaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/App.css25
-rw-r--r--frontend/src/components/ConfirmDialog.tsx5
-rw-r--r--frontend/src/components/Login.tsx38
-rw-r--r--frontend/src/components/MessageDialog.tsx3
-rw-r--r--frontend/src/components/Sidebar.tsx321
-rw-r--r--frontend/src/components/Sidebar_old.tsx210
-rw-r--r--frontend/src/components/UploadRunDialog.tsx9
-rw-r--r--frontend/src/css/Button.module.css91
-rw-r--r--frontend/src/css/Buttons.css3
-rw-r--r--frontend/src/css/Dialog.css5
-rw-r--r--frontend/src/css/Games.css6
-rw-r--r--frontend/src/css/Input.module.css15
-rw-r--r--frontend/src/css/Sidebar.module.css163
-rw-r--r--frontend/src/css/UploadRunDialog.css15
-rw-r--r--frontend/src/pages/About.tsx4
-rw-r--r--frontend/src/pages/Games.tsx4
-rw-r--r--frontend/src/pages/Profile.tsx2
-rw-r--r--frontend/src/types/Sidebar.ts12
18 files changed, 689 insertions, 242 deletions
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 14a9972..a6ef415 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,13 +1,11 @@
1main { 1main {
2 overflow: auto; 2 overflow: auto;
3 overflow-x: hidden; 3 overflow-x: hidden;
4 position: relative;
5 4
6 width: calc(100% - 380px); 5 width: calc(100% - 350px);
7 height: 100vh; 6 height: 100vh;
8 left: 350px;
9 7
10 padding-right: 30px; 8 padding: 0px 30px;
11 9
12 font-size: 40px; 10 font-size: 40px;
13 font-family: BarlowSemiCondensed-Regular; 11 font-family: BarlowSemiCondensed-Regular;
@@ -15,9 +13,28 @@ main {
15 13
16} 14}
17 15
16button img {
17 height: 24px;
18}
19
20b {
21 font-family: BarlowCondensed-Bold;
22}
23
24* {
25 --text-color: #cdcfdf;
26 --primary: #2B2E46;
27 --primary-dark: #202232;
28}
29
30#root {
31 display: flex;
32}
33
18a { 34a {
19 color: inherit; 35 color: inherit;
20 width: fit-content; 36 width: fit-content;
37 text-decoration: none;
21} 38}
22 39
23body { 40body {
diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx
index 44a653b..0679c25 100644
--- a/frontend/src/components/ConfirmDialog.tsx
+++ b/frontend/src/components/ConfirmDialog.tsx
@@ -1,5 +1,6 @@
1import React from 'react'; 1import React from 'react';
2 2
3import btn from "@css/Button.module.css"
3import "@css/Dialog.css" 4import "@css/Dialog.css"
4 5
5interface ConfirmDialogProps { 6interface ConfirmDialogProps {
@@ -20,8 +21,8 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ title, subtitle, onConfir
20 <span>{subtitle}</span> 21 <span>{subtitle}</span>
21 </div> 22 </div>
22 <div className='dialog-element dialog-btns-container'> 23 <div className='dialog-element dialog-btns-container'>
23 <button onClick={onCancel}>Cancel</button> 24 <button className={btn.default} onClick={onCancel}>Cancel</button>
24 <button onClick={onConfirm}>Confirm</button> 25 <button className={`${btn.default} ${btn.error}`} onClick={onConfirm}>Confirm</button>
25 </div> 26 </div>
26 </div> 27 </div>
27 </div> 28 </div>
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx
index f1628b2..fe0cbd1 100644
--- a/frontend/src/components/Login.tsx
+++ b/frontend/src/components/Login.tsx
@@ -5,14 +5,20 @@ import { ExitIcon, UserIcon, LoginIcon } from '@images/Images';
5import { UserProfile } from '@customTypes/Profile'; 5import { UserProfile } from '@customTypes/Profile';
6import { API } from '@api/Api'; 6import { API } from '@api/Api';
7import "@css/Login.css"; 7import "@css/Login.css";
8import { Button, Buttons } from "@customTypes/Sidebar";
9import btn from "@css/Button.module.css";
8 10
9interface LoginProps { 11interface LoginProps {
12 isSearching: boolean;
13 currentBtn: number;
14 buttonsList: Buttons;
15 setCurrentBtn: React.Dispatch<React.SetStateAction<number>>;
10 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 16 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
11 profile?: UserProfile; 17 profile?: UserProfile;
12 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 18 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
13}; 19};
14 20
15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { 21const Login: React.FC<LoginProps> = ({ isSearching, currentBtn, buttonsList, setCurrentBtn, setToken, profile, setProfile }) => {
16 22
17 const navigate = useNavigate(); 23 const navigate = useNavigate();
18 24
@@ -36,13 +42,15 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
36 {profile.profile ? 42 {profile.profile ?
37 ( 43 (
38 <> 44 <>
39 <Link to="/profile" tabIndex={-1} className='login'> 45 <Link to="/profile" tabIndex={-1}>
40 <button className='sidebar-button'> 46 <button onClick={() => {setCurrentBtn(buttonsList.top.length)}} id="sidebarBtn" className={`${btn.sidebar} ${btn.profile} ${currentBtn == buttonsList.top.length ? btn.selected : ""} ${isSearching ? btn.min : ""}`}>
41 <img className="avatar-img" src={profile.avatar_link} alt="" /> 47 <img className="avatar-img" src={profile.avatar_link} alt="" />
48 <span style={{justifyContent: "space-between", display: "flex", alignItems: "center", width: "100%"}}>
42 <span>{profile.user_name}</span> 49 <span>{profile.user_name}</span>
43 </button> 50 <button className={btn.logout} onClick={_logout}>
44 <button className='logout-button' onClick={_logout}> 51 <img src={ExitIcon} alt="" /><span />
45 <img src={ExitIcon} alt="" /><span /> 52 </button>
53 </span>
46 </button> 54 </button>
47 </Link> 55 </Link>
48 </> 56 </>
@@ -50,8 +58,8 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
50 : 58 :
51 ( 59 (
52 <> 60 <>
53 <Link to="/" tabIndex={-1} className='login'> 61 <Link to="/" tabIndex={-1}>
54 <button className='sidebar-button'> 62 <button id="sidebarBtn" className={`${btn.sidebar} ${btn.profile} ${isSearching ? btn.min : ""}`}>
55 <img className="avatar-img" src={profile.avatar_link} alt="" /> 63 <img className="avatar-img" src={profile.avatar_link} alt="" />
56 <span>Loading Profile...</span> 64 <span>Loading Profile...</span>
57 </button> 65 </button>
@@ -66,12 +74,12 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
66 ) 74 )
67 : 75 :
68 ( 76 (
69 <Link to="/api/v1/login" tabIndex={-1} className='login' > 77 <Link to="/api/v1/login" tabIndex={-1}>
70 <button className='sidebar-button' onClick={_login}> 78 <button id="sidebarBtn" className={`${btn.sidebar} ${isSearching ? btn.min : ""}`} onClick={_login}>
71 <img className="avatar-img" src={UserIcon} alt="" /> 79 <img className="avatar-img" src={UserIcon} alt="" />
72 <span> 80 <span>
73 <img src={LoginIcon} alt="Sign in through Steam" /> 81 <img src={LoginIcon} alt="Sign in through Steam" />
74 </span> 82 </span>
75 </button> 83 </button>
76 </Link> 84 </Link>
77 )} 85 )}
diff --git a/frontend/src/components/MessageDialog.tsx b/frontend/src/components/MessageDialog.tsx
index 5c85189..8c584b7 100644
--- a/frontend/src/components/MessageDialog.tsx
+++ b/frontend/src/components/MessageDialog.tsx
@@ -1,5 +1,6 @@
1import React from 'react'; 1import React from 'react';
2 2
3import btn from "@css/Button.module.css"
3import "@css/Dialog.css" 4import "@css/Dialog.css"
4 5
5interface MessageDialogProps { 6interface MessageDialogProps {
@@ -19,7 +20,7 @@ const MessageDialog: React.FC<MessageDialogProps> = ({ title, subtitle, onClose
19 <span>{subtitle}</span> 20 <span>{subtitle}</span>
20 </div> 21 </div>
21 <div className='dialog-element dialog-btns-container'> 22 <div className='dialog-element dialog-btns-container'>
22 <button onClick={onClose}>Close</button> 23 <button className={btn.default} onClick={onClose}>Close</button>
23 </div> 24 </div>
24 </div> 25 </div>
25 </div> 26 </div>
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 67f7f3d..beff4f0 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -1,12 +1,15 @@
1import React from 'react'; 1import React from "react";
2import { Link, useLocation } from 'react-router-dom'; 2import { Link, useLocation } from 'react-router-dom';
3
4import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images'; 3import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images';
5import Login from '@components/Login';
6import { UserProfile } from '@customTypes/Profile'; 4import { UserProfile } from '@customTypes/Profile';
7import { Search } from '@customTypes/Search'; 5import sidebar from "@css/Sidebar.module.css";
6import { Button, Buttons } from "@customTypes/Sidebar";
7import btn from "@css/Button.module.css";
8import { abort } from "process";
9import Login from "@components/Login";
8import { API } from '@api/Api'; 10import { API } from '@api/Api';
9import "@css/Sidebar.css"; 11import inp from "@css/Input.module.css";
12import { Search } from '@customTypes/Search';
10 13
11interface SidebarProps { 14interface SidebarProps {
12 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 15 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
@@ -16,187 +19,131 @@ interface SidebarProps {
16}; 19};
17 20
18const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUploadRun }) => { 21const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUploadRun }) => {
19 22 const location = useLocation();
20 const [searchData, setSearchData] = React.useState<Search | undefined>(undefined); 23 const [load, setLoad] = React.useState<boolean>(false);
21 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); 24 const [searchData, setSearchData] = React.useState<Search | undefined>(undefined);
22 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); 25 const [hasClickedSearch, setHasClickedSearch] = React.useState<boolean>(false);
23 26 const [isSearching, setIsSearching] = React.useState<boolean>(false);
24 const location = useLocation(); 27 const [buttonsList, setButtonsList] = React.useState<Buttons>({
25 const path = location.pathname; 28 top: [
26 29 {img: HomeIcon, text: "Home", url: "/"},
27 const handle_sidebar_click = (clicked_sidebar_idx: number) => { 30 {img: PortalIcon, text: "Games", url: "/games"},
28 const btn = document.querySelectorAll("button.sidebar-button"); 31 {img: FlagIcon, text: "Rankings", url: "/rankings"}
29 if (isSidebarOpen) { setSidebarOpen(false); _handle_sidebar_hide() } 32 ],
30 // clusterfuck 33 bottom: [
31 btn.forEach((e, i) => { 34 {img: BookIcon, text: "Rules", url: "/rules"},
32 btn[i].classList.remove("sidebar-button-selected") 35 {img: HelpIcon, text: "About LPHUB", url: "/about"}
33 btn[i].classList.add("sidebar-button-deselected") 36 ]
34 }) 37 });
35 btn[clicked_sidebar_idx].classList.add("sidebar-button-selected") 38
36 btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected") 39 const _handle_search = () => {
37 }; 40 if (!hasClickedSearch) {
38 41 _handle_search_change("");
39 const _handle_sidebar_hide = () => { 42 }
40 var btn = document.querySelectorAll("button.sidebar-button") as NodeListOf<HTMLElement> 43 setHasClickedSearch(true);
41 const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement> 44 setIsSearching(!isSearching);
42 const side = document.querySelector("#sidebar-list") as HTMLElement; 45 document.querySelector<HTMLInputElement>("#searchInput")!.focus();
43 const searchbar = document.querySelector("#searchbar") as HTMLInputElement; 46 }
44 const uploadRunBtn = document.querySelector("#upload-run") as HTMLInputElement; 47
45 const uploadRunSpan = document.querySelector("#upload-run>span") as HTMLInputElement; 48 const _handle_search_change = async (query: string) => {
46 49 const response = await API.get_search(query);
47 if (isSidebarOpen) { 50 setSearchData(response);
48 if (profile) { 51 }
49 const login = document.querySelectorAll(".login>button")[1] as HTMLElement; 52
50 login.style.opacity = "1" 53 const _get_index_load = () => {
51 uploadRunBtn.style.width = "310px" 54 const pathname = window.location.pathname;
52 uploadRunBtn.style.padding = "0.4em 0 0 11px" 55 const btnObj = buttonsList.top.find(obj => obj.url === pathname);
53 uploadRunSpan.style.opacity = "0" 56 let btnIndex = buttonsList.top.findIndex(obj => obj.url === pathname);
54 setTimeout(() => { 57 if (btnIndex != -1) {
55 uploadRunSpan.style.opacity = "1" 58 return btnIndex;
56 }, 100) 59 } else if (buttonsList.top.findIndex(obj => obj.url === pathname) == -1 && buttonsList.bottom.findIndex(obj => obj.url === pathname) != -1) {
57 } 60 btnIndex = buttonsList.bottom.findIndex(obj => obj.url === pathname);
58 setSidebarOpen(false); 61 return btnIndex + buttonsList.top.length + 1;
59 side.style.width = "320px" 62 } else if (load) {
60 btn.forEach((e, i) => { 63 return currentBtn;
61 e.style.width = "310px" 64 } else {
62 e.style.padding = "0.4em 0 0 11px" 65 return 0;
63 setTimeout(() => { 66 }
64 span[i].style.opacity = "1" 67 }
65 }, 100) 68 const [currentBtn, setCurrentBtn] = React.useState<number>(_get_index_load);
66 }); 69
67 side.style.zIndex = "2" 70 React.useEffect(() => {
68 } else { 71 setCurrentBtn(_get_index_load);
69 if (profile) { 72 setLoad(true);
70 const login = document.querySelectorAll(".login>button")[1] as HTMLElement; 73 }, [location])
71 login.style.opacity = "0" 74
72 uploadRunBtn.style.width = "40px" 75 return (
73 uploadRunBtn.style.padding = "0.4em 0 0 5px" 76 <section className={sidebar.sidebar}>
74 uploadRunSpan.style.opacity = "0" 77 <div className={sidebar.logo}>
75 } 78 <Link onClick={isSearching ? _handle_search : () => {}} to={"/"}>
76 setSidebarOpen(true); 79 <img src={LogoIcon}/>
77 side.style.width = "40px"; 80 <div>
78 searchbar.focus(); 81 <span className={sidebar.logoTitle}><b>PORTAL 2</b></span>
79 btn.forEach((e, i) => { 82 <span>Least Portals Hub</span>
80 e.style.width = "40px" 83 </div>
81 e.style.padding = "0.4em 0 0 5px" 84 </Link>
82 span[i].style.opacity = "0" 85 </div>
83 }) 86
84 setTimeout(() => { 87 <div className={sidebar.btnsContainer} style={{height: "calc(100% - 104px)"}}>
85 side.style.zIndex = "0" 88 <div className={`${sidebar.btns} ${isSearching ? sidebar.min : ""}`}>
86 }, 300); 89 <div className={sidebar.topBtns}>
87 } 90 <button onClick={_handle_search} className={`${btn.sidebar} ${isSearching ? btn.min : ""}`}>
88 }; 91 <img src={SearchIcon}/>
89 92 <span>Search</span>
90 const _handle_sidebar_lock = () => { 93 </button>
91 if (!isSidebarLocked) { 94
92 _handle_sidebar_hide() 95 <span></span>
93 setIsSidebarLocked(true); 96
94 setTimeout(() => setIsSidebarLocked(false), 300); 97 {buttonsList.top.map((e: any, i: any) => {
95 } 98 return <Link to={e.url}><button onClick={isSearching ? _handle_search : () => {}} className={`${btn.sidebar} ${currentBtn == i ? btn.selected : ""} ${isSearching ? btn.min : ""}`} key={i}>
96 }; 99 <img src={e.img}/>
97 100 <span>{e.text}</span>
98 const _handle_search_change = async (q: string) => { 101 </button></Link>
99 const searchResponse = await API.get_search(q); 102 })
100 setSearchData(searchResponse); 103
101 }; 104 }
102 105 </div>
103 React.useEffect(() => { 106 <div className={sidebar.bottomBtns}>
104 if (path === "/") { handle_sidebar_click(1) } 107 <Login isSearching={isSearching} setCurrentBtn={setCurrentBtn} currentBtn={currentBtn} buttonsList={buttonsList} setToken={setToken} profile={profile} setProfile={setProfile}/>
105 else if (path.includes("games")) { handle_sidebar_click(2) } 108
106 else if (path.includes("rankings")) { handle_sidebar_click(3) } 109 {buttonsList.bottom.map((e: any, i: any) => {
107 // else if (path.includes("news")) { handle_sidebar_click(4) } 110 return <Link to={e.url}><button onClick={isSearching ? _handle_search : () => {}} key={i} className={`${btn.sidebar} ${currentBtn == i + buttonsList.top.length + 1 ? btn.selected : ""} ${isSearching ? btn.min : ""}`}>
108 // else if (path.includes("scorelog")) { handle_sidebar_click(5) } 111 <img src={e.img}/>
109 else if (path.includes("profile")) { handle_sidebar_click(4) } 112 <span>{e.text}</span>
110 else if (path.includes("rules")) { handle_sidebar_click(5) } 113 </button></Link>
111 else if (path.includes("about")) { handle_sidebar_click(6) } 114 })
112 }, [path]); 115
113 116 }
114 return ( 117 </div>
115 <div id='sidebar'> 118 </div>
116 <Link to="/" tabIndex={-1}> 119
117 <div id='logo'> {/* logo */} 120 <div className={`${sidebar.searchContainer} ${isSearching ? sidebar.min : ""}`}>
118 <img src={LogoIcon} alt="" height={"80px"} /> 121 <div className={sidebar.inpContainer}>
119 <div id='logo-text'> 122 <input onChange={(e) => {_handle_search_change(e.target.value)}} id="searchInput" className={inp.sidebar} type="text" placeholder='Search for map or a player...'/>
120 <span><b>PORTAL 2</b></span><br /> 123 </div>
121 <span>Least Portals Hub</span> 124
122 </div> 125 <div className={sidebar.searchResults}>
123 </div> 126 {searchData?.maps.map((map, i) => {
124 </Link> 127 return <Link style={{animationDelay: `${i < 30 ? i * 0.05 : 0}s`}} className={sidebar.result} to={`/maps/${map.id}`} key={i}>
125 <div id='sidebar-list'> {/* List */} 128 <span>{map.game}</span>
126 <div id='sidebar-toplist'> {/* Top */} 129 <span>{map.chapter}</span>
127 130 <span>{map.map}</span>
128 <button className='sidebar-button' onClick={() => _handle_sidebar_lock()}><img src={SearchIcon} alt="" /><span>Search</span></button> 131 </Link>
129 132 })}
130 <span></span> 133
131 134 {searchData?.players.map((player, i) => {
132 <Link to="/" tabIndex={-1}> 135 return <Link className={`${sidebar.result} ${sidebar.player}`} to={`/users/${player.steam_id}`}>
133 <button className='sidebar-button'><img src={HomeIcon} alt="homepage" /><span>Home&nbsp;Page</span></button> 136 <img src={player.avatar_link}/>
134 </Link> 137 <span>{player.user_name}</span>
135 138 </Link>
136 <Link to="/games" tabIndex={-1}> 139 })}
137 <button className='sidebar-button'><img src={PortalIcon} alt="games" /><span>Games</span></button> 140 </div>
138 </Link> 141 </div>
139 142
140 <Link to="/rankings" tabIndex={-1}> 143 </div>
141 <button className='sidebar-button'><img src={FlagIcon} alt="rankings" /><span>Rankings</span></button> 144 </section>
142 </Link> 145 )
143 146}
144 {/* <Link to="/news" tabIndex={-1}>
145 <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button>
146 </Link> */}
147
148 {/* <Link to="/scorelog" tabIndex={-1}>
149 <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score&nbsp;Logs</span></button>
150 </Link> */}
151 </div>
152 <div id='sidebar-bottomlist'>
153 <span></span>
154
155 {
156 profile && profile.profile ?
157 <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Upload&nbsp;Record</span></button>
158 :
159 <span></span>
160 }
161
162 <Login setToken={setToken} profile={profile} setProfile={setProfile} />
163
164 <Link to="/rules" tabIndex={-1}>
165 <button className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard&nbsp;Rules</span></button>
166 </Link>
167
168 <Link to="/about" tabIndex={-1}>
169 <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About&nbsp;LPHUB</span></button>
170 </Link>
171 </div>
172 </div>
173 <div>
174 <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} />
175
176 <div id='search-data'>
177
178 {searchData?.maps.map((q, index) => (
179 <Link to={`/maps/${q.id}`} className='search-map' key={index}>
180 <span>{q.game}</span>
181 <span>{q.chapter}</span>
182 <span>{q.map}</span>
183 </Link>
184 ))}
185 {searchData?.players.map((q, index) =>
186 (
187 <Link to={
188 profile && q.steam_id === profile.steam_id ? `/profile` :
189 `/users/${q.steam_id}`
190 } className='search-player' key={index}>
191 <img src={q.avatar_link} alt='pfp'></img>
192 <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span>
193 </Link>
194 ))}
195
196 </div>
197 </div>
198 </div>
199 );
200};
201 147
202export default Sidebar; 148export default Sidebar;
149
diff --git a/frontend/src/components/Sidebar_old.tsx b/frontend/src/components/Sidebar_old.tsx
new file mode 100644
index 0000000..4d1cd7a
--- /dev/null
+++ b/frontend/src/components/Sidebar_old.tsx
@@ -0,0 +1,210 @@
1import React, { useRef } from 'react';
2import { Link, useLocation } from 'react-router-dom';
3
4import btn from "@css/Button.module.css";
5import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images';
6import Login from '@components/Login';
7import { UserProfile } from '@customTypes/Profile';
8import { Search } from '@customTypes/Search';
9import { API } from '@api/Api';
10import "@css/Sidebar.css";
11
12interface SidebarProps {
13 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
14 profile?: UserProfile;
15 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
16 onUploadRun: () => void;
17};
18
19const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUploadRun }) => {
20
21 const btnRef = useRef(null);
22 const [searchData, setSearchData] = React.useState<Search | undefined>(undefined);
23 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false);
24 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true);
25
26 const location = useLocation();
27 const path = location.pathname;
28
29 const handle_sidebar_click = (clicked_sidebar_idx: number) => {
30 const btn = document.querySelectorAll("#sidebarBtn");
31 if (isSidebarOpen) { setSidebarOpen(false); _handle_sidebar_hide() }
32 // clusterfuck
33 btn.forEach((e, i) => {
34 btn[i].classList.remove("sidebar-button-selected")
35 btn[i].classList.add("sidebar-button-deselected")
36 })
37 btn[clicked_sidebar_idx].classList.add("sidebar-button-selected")
38 btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected")
39 };
40
41 const _handle_sidebar_hide = () => {
42 var btn = document.querySelectorAll("button.sidebar-button") as NodeListOf<HTMLElement>
43 const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement>
44 const side = document.querySelector("#sidebar-list") as HTMLElement;
45 const searchbar = document.querySelector("#searchbar") as HTMLInputElement;
46 const uploadRunBtn = document.querySelector("#upload-run") as HTMLInputElement;
47 const uploadRunSpan = document.querySelector("#upload-run>span") as HTMLInputElement;
48
49 if (isSidebarOpen) {
50 if (profile) {
51 const login = document.querySelectorAll(".login>button")[1] as HTMLElement;
52 login.style.opacity = "1"
53 uploadRunBtn.style.width = "310px"
54 uploadRunBtn.style.padding = "0.4em 0 0 11px"
55 uploadRunSpan.style.opacity = "0"
56 setTimeout(() => {
57 uploadRunSpan.style.opacity = "1"
58 }, 100)
59 }
60 setSidebarOpen(false);
61 side.style.width = "320px"
62 btn.forEach((e, i) => {
63 e.style.width = "310px"
64 e.style.padding = "0.4em 0 0 11px"
65 setTimeout(() => {
66 span[i].style.opacity = "1"
67 }, 100)
68 });
69 side.style.zIndex = "2"
70 } else {
71 if (profile) {
72 const login = document.querySelectorAll(".login>button")[1] as HTMLElement;
73 login.style.opacity = "0"
74 uploadRunBtn.style.width = "40px"
75 uploadRunBtn.style.padding = "0.4em 0 0 5px"
76 uploadRunSpan.style.opacity = "0"
77 }
78 setSidebarOpen(true);
79 side.style.width = "40px";
80 searchbar.focus();
81 btn.forEach((e, i) => {
82 e.style.width = "40px"
83 e.style.padding = "0.4em 0 0 5px"
84 span[i].style.opacity = "0"
85 })
86 setTimeout(() => {
87 side.style.zIndex = "0"
88 }, 300);
89 }
90 };
91
92 const _handle_sidebar_lock = () => {
93 if (!isSidebarLocked) {
94 _handle_sidebar_hide()
95 setIsSidebarLocked(true);
96 setTimeout(() => setIsSidebarLocked(false), 300);
97 }
98 };
99
100 const _handle_search_change = async (q: string) => {
101 const searchResponse = await API.get_search(q);
102 setSearchData(searchResponse);
103 };
104
105 React.useEffect(() => {
106 if (path === "/") { handle_sidebar_click(1) }
107 else if (path.includes("games")) { handle_sidebar_click(2) }
108 else if (path.includes("rankings")) { handle_sidebar_click(3) }
109 // else if (path.includes("news")) { handle_sidebar_click(4) }
110 // else if (path.includes("scorelog")) { handle_sidebar_click(5) }
111 else if (path.includes("profile")) { handle_sidebar_click(4) }
112 else if (path.includes("rules")) { handle_sidebar_click(5) }
113 else if (path.includes("about")) { handle_sidebar_click(6) }
114 }, [path]);
115
116 React.useEffect(() => {
117 const btns = document.querySelectorAll("#sidebarBtn");
118 btns.forEach((e, num) => {
119 e.setAttribute("num", num.toString());
120 });
121 })
122
123 return (
124 <div id='sidebar'>
125 <Link to="/" tabIndex={-1}>
126 <div id='logo'> {/* logo */}
127 <img src={LogoIcon} alt="" height={"80px"} />
128 <div id='logo-text'>
129 <span><b>PORTAL 2</b></span><br />
130 <span>Least Portals Hub</span>
131 </div>
132 </div>
133 </Link>
134 <div id='sidebar-list'> {/* List */}
135 <div id='sidebar-toplist'> {/* Top */}
136
137 <button id="sidebarBtn" className='sidebar-button' onClick={() => _handle_sidebar_lock()}><img src={SearchIcon} alt="" /><span>Search</span></button>
138
139 <span></span>
140
141 <Link to="/" tabIndex={-1}>
142 <button ref={btnRef} id="sidebarBtn" className={`${btn.sidebar}`}><img src={HomeIcon} alt="homepage" /><span>Home&nbsp;Page</span></button>
143 </Link>
144
145 <Link to="/games" tabIndex={-1}>
146 <button id="sidebarBtn" className='sidebar-button'><img src={PortalIcon} alt="games" /><span>Games</span></button>
147 </Link>
148
149 <Link to="/rankings" tabIndex={-1}>
150 <button id="sidebarBtn" className='sidebar-button'><img src={FlagIcon} alt="rankings" /><span>Rankings</span></button>
151 </Link>
152
153 {/* <Link to="/news" tabIndex={-1}>
154 <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button>
155 </Link> */}
156
157 {/* <Link to="/scorelog" tabIndex={-1}>
158 <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score&nbsp;Logs</span></button>
159 </Link> */}
160 </div>
161 <div id='sidebar-bottomlist'>
162 <span></span>
163
164 {
165 profile && profile.profile ?
166 <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Upload&nbsp;Record</span></button>
167 :
168 <span></span>
169 }
170
171
172 <Link to="/rules" tabIndex={-1}>
173 <button id="sidebarBtn" className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard&nbsp;Rules</span></button>
174 </Link>
175
176 <Link to="/about" tabIndex={-1}>
177 <button id="sidebarBtn" className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About&nbsp;LPHUB</span></button>
178 </Link>
179 </div>
180 </div>
181 <div>
182 <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} />
183
184 <div id='search-data'>
185
186 {searchData?.maps.map((q, index) => (
187 <Link to={`/maps/${q.id}`} className='search-map' key={index}>
188 <span>{q.game}</span>
189 <span>{q.chapter}</span>
190 <span>{q.map}</span>
191 </Link>
192 ))}
193 {searchData?.players.map((q, index) =>
194 (
195 <Link to={
196 profile && q.steam_id === profile.steam_id ? `/profile` :
197 `/users/${q.steam_id}`
198 } className='search-player' key={index}>
199 <img src={q.avatar_link} alt='pfp'></img>
200 <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span>
201 </Link>
202 ))}
203
204 </div>
205 </div>
206 </div>
207 );
208};
209
210export default Sidebar;
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx
index 118b589..951944b 100644
--- a/frontend/src/components/UploadRunDialog.tsx
+++ b/frontend/src/components/UploadRunDialog.tsx
@@ -2,6 +2,7 @@ import React from 'react';
2import { UploadRunContent } from '@customTypes/Content'; 2import { UploadRunContent } from '@customTypes/Content';
3import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from '@nekz/sdp'; 3import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from '@nekz/sdp';
4 4
5import btn from "@css/Button.module.css";
5import '@css/UploadRunDialog.css'; 6import '@css/UploadRunDialog.css';
6import { Game } from '@customTypes/Game'; 7import { Game } from '@customTypes/Game';
7import { Map } from '@customTypes/Map'; 8import { Map } from '@customTypes/Map';
@@ -241,7 +242,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
241 <span>Drag and drop</span> 242 <span>Drag and drop</span>
242 <div> 243 <div>
243 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> 244 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br />
244 <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> 245 <button className={btn.default}>Upload</button>
245 </div> 246 </div>
246 </div> 247 </div>
247 : null} 248 : null}
@@ -260,7 +261,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
260 <span>Drag and drop</span> 261 <span>Drag and drop</span>
261 <div> 262 <div>
262 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> 263 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br />
263 <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> 264 <button className={btn.default}>Upload</button>
264 </div> 265 </div>
265 </div> 266 </div>
266 : null} 267 : null}
@@ -281,8 +282,8 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
281 } 282 }
282 </div> 283 </div>
283 <div className='upload-run-buttons-container'> 284 <div className='upload-run-buttons-container'>
284 <button onClick={_upload_run}>Submit</button> 285 <button className={`${btn.defaultWide}`} onClick={_upload_run}>Submit</button>
285 <button onClick={() => onClose(false)}>Cancel</button> 286 <button className={`${btn.defaultWide}`} onClick={() => onClose(false)}>Cancel</button>
286 </div> 287 </div>
287 </div> 288 </div>
288 </div> 289 </div>
diff --git a/frontend/src/css/Button.module.css b/frontend/src/css/Button.module.css
new file mode 100644
index 0000000..d1c3ad7
--- /dev/null
+++ b/frontend/src/css/Button.module.css
@@ -0,0 +1,91 @@
1.default, .defaultWide, .sidebar, .logout {
2 border: none;
3 border-radius: 24px;
4 padding: 5px 10px;
5 background-color: #2b2e46;
6 font-family: BarlowSemiCondensed-Regular;
7 color: #CDCFDF;
8 font-size: 18px;
9 cursor: pointer;
10 transition: all 0.2s ease;
11}
12
13.sidebar.selected {
14 background-color: #202232;
15}
16
17.sidebar.selected:hover {
18 background-color: #202232;
19}
20
21.default:hover, .defaultWide:hover, .sidebar:hover {
22 background-color: rgb(38, 42, 62);
23}
24
25.defaultWide {
26 width: 100%;
27}
28
29.default.error, .defaultWide.error {
30 background-color: rgb(147, 45, 45);
31}
32
33.default.error:hover, .defaultWide.error {
34 background-color: rgb(105, 36, 36);
35}
36
37.sidebar {
38 display: flex;
39 width: 100%;
40 align-items: center;
41 font-family: BarlowSemiCondensed-Regular;
42 padding: 8px 12px;
43 height: 42px;
44 padding-right: 4px;
45 text-wrap: nowrap;
46 transition: all 0.2s ease;
47}
48
49.sidebar.min {
50 padding: 8px 9px;
51}
52
53.sidebar.min>span {
54 opacity: 0;
55 animation: sidebar_text_out 0.2s ease;
56 transform: translateX(-200px);
57}
58
59.sidebar img {
60 height: 24px;
61}
62
63.sidebar.profile>img {
64 border-radius: 24px;
65}
66
67.sidebar>span {
68 padding: 0px 8px;
69 transition: all 0.2s ease;
70}
71
72.logout {
73 background-color: #00000000;
74 display: flex;
75 align-items: center;
76}
77
78@keyframes sidebar_text_out {
79 0% {
80 opacity: 1;
81 transform: translateX(0px);
82 }
83 50% {
84 opacity: 0;
85 transform: translateX(0px);
86 }
87 60%, 100% {
88 transform: translateX(-200px);
89 }
90}
91
diff --git a/frontend/src/css/Buttons.css b/frontend/src/css/Buttons.css
new file mode 100644
index 0000000..de8f31d
--- /dev/null
+++ b/frontend/src/css/Buttons.css
@@ -0,0 +1,3 @@
1.default {
2
3}
diff --git a/frontend/src/css/Dialog.css b/frontend/src/css/Dialog.css
index fc557d2..51bf0ae 100644
--- a/frontend/src/css/Dialog.css
+++ b/frontend/src/css/Dialog.css
@@ -61,11 +61,6 @@
61 white-space: pre-wrap; 61 white-space: pre-wrap;
62} 62}
63 63
64.dialog-btns-container button {
65 border-radius: 24px;
66 padding: 5px 10px;
67}
68
69.dialog-btns-container button:nth-child(2):hover { 64.dialog-btns-container button:nth-child(2):hover {
70 background-color: rgb(105, 36, 36); 65 background-color: rgb(105, 36, 36);
71} 66}
diff --git a/frontend/src/css/Games.css b/frontend/src/css/Games.css
index ec57a71..9270a98 100644
--- a/frontend/src/css/Games.css
+++ b/frontend/src/css/Games.css
@@ -11,9 +11,7 @@
11} 11}
12 12
13.games-page-item-content { 13.games-page-item-content {
14 position: absolute; 14 position: relative;
15 left: 50px;
16 width: calc(100% - 100px);
17} 15}
18 16
19.games-page-item-content a { 17.games-page-item-content a {
@@ -96,4 +94,4 @@ span>b {
96.games-page-item-body-item-num { 94.games-page-item-body-item-num {
97 font-size: 50px; 95 font-size: 50px;
98 font-family: BarlowCondensed-Bold; 96 font-family: BarlowCondensed-Bold;
99} \ No newline at end of file 97}
diff --git a/frontend/src/css/Input.module.css b/frontend/src/css/Input.module.css
new file mode 100644
index 0000000..c216f73
--- /dev/null
+++ b/frontend/src/css/Input.module.css
@@ -0,0 +1,15 @@
1.sidebar {
2 background-color: #161723;
3 color: var(--text-color);
4 border: none;
5 border-radius: 300px;
6 font-family: BarlowSemiCondensed-Regular;
7 padding: 8px;
8 width: calc(100% - 16px);
9 outline: none;
10 font-size: 18px;
11}
12
13.sidebar::placehoder {
14 color: #2b2e46;
15}
diff --git a/frontend/src/css/Sidebar.module.css b/frontend/src/css/Sidebar.module.css
new file mode 100644
index 0000000..9436a93
--- /dev/null
+++ b/frontend/src/css/Sidebar.module.css
@@ -0,0 +1,163 @@
1.sidebar {
2 display: flex;
3 width: 350px;
4 min-width: 350px;
5 height: 100vh;
6 color: var(--text-color);
7 font-family: BarlowSemiCondensed-Regular;
8 font-size: 18px;
9 background-color: var(--primary-dark);
10 flex-direction: column;
11}
12
13.logo, .logo>a {
14 height: fit-content;
15 display: flex;
16 width: calc(100% - 4px);
17 text-decoration: none;
18 padding-left: 4px;
19 background-color: var(--primary)
20}
21
22.logo>a>img {
23 padding: 12px 8px;
24 height: 80px;
25}
26
27.logo>a>div {
28 display: flex;
29 flex-direction: column;
30 width: 100%;
31}
32
33.logo>a>div>span {
34 font-size: 36px;
35 font-family: BarlowCondensed-Regular;
36 padding-left: 8px;
37}
38
39.logoTitle {
40 display: flex;
41 height: 60px;
42 font-size: 56px;
43}
44
45/* btns */
46.btns {
47 display: flex;
48 flex-direction: column;
49 padding: 0px 8px;
50 height: 100%;
51 width: calc(100% - 16px);
52 justify-content: space-between;
53 background-color: var(--primary);
54 transition: all 0.3s ease;
55}
56
57.topBtns>span {
58 height: 28px;
59 display: flex;
60}
61
62.btns.min {
63 width: 42px;
64}
65
66.topBtns, .bottomBtns {
67 display: flex;
68 flex-direction: column;
69 gap: 8px;
70}
71
72.topBtns {
73 padding-top: 8px;
74}
75
76.bottomBtns {
77 padding-bottom: 8px;
78}
79
80.topBtns a, .bottomBtns a {
81 width: 100%;
82}
83
84.btnsContainer {
85 display: flex;
86}
87
88/* Clusterfuck */
89.searchContainer {
90 width: 0%;
91 transition: all 0.3s ease, overflow-y 0.0s ease 0.1s;
92 opacity: 0;
93 overflow-y: hidden;
94}
95
96.searchContainer.min {
97 width: 100%;
98 transition: all 0.3s ease, opacity 0.1s ease 0.1s;
99 opacity: 1;
100 overflow-y: auto;
101}
102
103.inpContainer {
104 padding: 8px;
105}
106
107.searchResults {
108 overflow-y: auto;
109 height: calc(100% - 54px);
110}
111
112.result {
113 margin: 10px 6px 0 6px;
114 height: 80px;
115 width: 100%;
116 max-width: 285px;
117 animation: result_in 0.2s ease;
118 animation-fill-mode: backwards;
119 overflow: hidden;
120
121 border-radius: 20px;
122 text-align: center;
123
124 display: grid;
125
126 border: 0;
127 transition: background-color .1s;
128 background-color: #2b2e46;
129 grid-template-rows: 20% 20% 60%;
130 width: calc(100% - 15px);
131}
132.result>span{
133 color: #888;
134 font-size: 16px;
135 font-family: BarlowSemiCondensed-Regular;
136}
137.result>span:nth-child(3), .result.player span{
138 font-size: 30px;
139 color: #CDCFDF;
140}
141
142.result.player img {
143 height: 80px;
144}
145
146.result.player span {
147 display: flex;
148 text-align: left;
149 margin-left: 90px;
150 width: fit-content;
151}
152
153@keyframes result_in {
154 0% {
155 opacity: 0;
156 transform: translateY(20px);
157 }
158 100% {
159 opacity: 1;
160 transform: translateY(0px);
161 }
162}
163
diff --git a/frontend/src/css/UploadRunDialog.css b/frontend/src/css/UploadRunDialog.css
index f129bb8..7cc2cf5 100644
--- a/frontend/src/css/UploadRunDialog.css
+++ b/frontend/src/css/UploadRunDialog.css
@@ -96,25 +96,10 @@ div#upload-run{
96 } 96 }
97} 97}
98 98
99button, input {
100 background-color: #2b2e46;
101 border: none;
102 font-family: BarlowSemiCondensed-Regular;
103 color: #CDCFDF;
104 font-size: 18px;
105 cursor: pointer;
106 padding: 5px 0px;
107 transition: all 0.2s ease;
108}
109
110.upload-run-buttons-container button { 99.upload-run-buttons-container button {
111 border-radius: 32px; 100 border-radius: 32px;
112} 101}
113 102
114button:hover {
115 background-color: #222538;
116}
117
118.upload-run-map-container { 103.upload-run-map-container {
119 display: flex; 104 display: flex;
120} 105}
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx
index ded3461..fe2e25a 100644
--- a/frontend/src/pages/About.tsx
+++ b/frontend/src/pages/About.tsx
@@ -27,9 +27,9 @@ const About: React.FC = () => {
27 27
28 28
29 return ( 29 return (
30 <div id="about"> 30 <main>
31 <ReactMarkdown>{aboutText}</ReactMarkdown> 31 <ReactMarkdown>{aboutText}</ReactMarkdown>
32 </div> 32 </main>
33 ); 33 );
34}; 34};
35 35
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx
index 15105c9..e7e031e 100644
--- a/frontend/src/pages/Games.tsx
+++ b/frontend/src/pages/Games.tsx
@@ -25,7 +25,7 @@ const Games: React.FC<GamesProps> = ({ games }) => {
25 }, []); 25 }, []);
26 26
27 return ( 27 return (
28 <div className='games-page'> 28 <main>
29 <section> 29 <section>
30 <div className='games-page-content'> 30 <div className='games-page-content'>
31 <div className='games-page-item-content'> 31 <div className='games-page-item-content'>
@@ -35,7 +35,7 @@ const Games: React.FC<GamesProps> = ({ games }) => {
35 </div> 35 </div>
36 </div> 36 </div>
37 </section> 37 </section>
38 </div> 38 </main>
39 ); 39 );
40}; 40};
41 41
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
index f7134a7..00d8f4e 100644
--- a/frontend/src/pages/Profile.tsx
+++ b/frontend/src/pages/Profile.tsx
@@ -108,7 +108,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
108 }; 108 };
109 109
110 return ( 110 return (
111 <div> 111 <div style={{position: "absolute", width: "calc(100% - 50px)", left: "350px"}}>
112 {MessageDialogComponent} 112 {MessageDialogComponent}
113 {MessageDialogLoadComponent} 113 {MessageDialogLoadComponent}
114 {ConfirmDialogComponent} 114 {ConfirmDialogComponent}
diff --git a/frontend/src/types/Sidebar.ts b/frontend/src/types/Sidebar.ts
new file mode 100644
index 0000000..71a7571
--- /dev/null
+++ b/frontend/src/types/Sidebar.ts
@@ -0,0 +1,12 @@
1import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images';
2
3export interface Button {
4 img: string;
5 text: string;
6 url: string;
7}
8
9export interface Buttons {
10 top: Button[];
11 bottom: Button[];
12}