diff options
23 files changed, 799 insertions, 281 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 @@ | |||
| 1 | main { | 1 | main { |
| 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 | ||
| 16 | button img { | ||
| 17 | height: 24px; | ||
| 18 | } | ||
| 19 | |||
| 20 | b { | ||
| 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 | |||
| 18 | a { | 34 | a { |
| 19 | color: inherit; | 35 | color: inherit; |
| 20 | width: fit-content; | 36 | width: fit-content; |
| 37 | text-decoration: none; | ||
| 21 | } | 38 | } |
| 22 | 39 | ||
| 23 | body { | 40 | body { |
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 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | 2 | ||
| 3 | import btn from "@css/Button.module.css" | ||
| 3 | import "@css/Dialog.css" | 4 | import "@css/Dialog.css" |
| 4 | 5 | ||
| 5 | interface ConfirmDialogProps { | 6 | interface 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/GameCategory.tsx b/frontend/src/components/GameCategory.tsx index d8879ef..a568a8f 100644 --- a/frontend/src/components/GameCategory.tsx +++ b/frontend/src/components/GameCategory.tsx | |||
| @@ -2,7 +2,7 @@ import React from 'react'; | |||
| 2 | import { Link } from "react-router-dom"; | 2 | import { Link } from "react-router-dom"; |
| 3 | 3 | ||
| 4 | import { Game, GameCategoryPortals } from '@customTypes/Game'; | 4 | import { Game, GameCategoryPortals } from '@customTypes/Game'; |
| 5 | import "@css/Games.css" | 5 | import info from "@css/Info.module.css"; |
| 6 | 6 | ||
| 7 | interface GameCategoryProps { | 7 | interface GameCategoryProps { |
| 8 | game: Game; | 8 | game: Game; |
| @@ -11,11 +11,11 @@ interface GameCategoryProps { | |||
| 11 | 11 | ||
| 12 | const GameCategory: React.FC<GameCategoryProps> = ({cat, game}) => { | 12 | const GameCategory: React.FC<GameCategoryProps> = ({cat, game}) => { |
| 13 | return ( | 13 | return ( |
| 14 | <Link className="games-page-item-body-item" to={"/games/" + game.id + "?cat=" + cat.category.id}> | 14 | <Link className={info.infoBlock} to={"/games/" + game.id + "?cat=" + cat.category.id}> |
| 15 | <div> | 15 | <div> |
| 16 | <span className='games-page-item-body-item-title'>{cat.category.name}</span> | 16 | <span>{cat.category.name}</span> |
| 17 | <br /> | 17 | <br /> |
| 18 | <span className='games-page-item-body-item-num'>{cat.portal_count}</span> | 18 | <span>{cat.portal_count}</span> |
| 19 | </div> | 19 | </div> |
| 20 | </Link> | 20 | </Link> |
| 21 | ) | 21 | ) |
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx index 3bd2842..2fba85a 100644 --- a/frontend/src/components/GameEntry.tsx +++ b/frontend/src/components/GameEntry.tsx | |||
| @@ -2,7 +2,7 @@ import React from 'react'; | |||
| 2 | import { Link } from "react-router-dom"; | 2 | import { Link } from "react-router-dom"; |
| 3 | 3 | ||
| 4 | import { Game, GameCategoryPortals } from '@customTypes/Game'; | 4 | import { Game, GameCategoryPortals } from '@customTypes/Game'; |
| 5 | import "@css/Games.css" | 5 | import games from "@css/Games.module.css"; |
| 6 | 6 | ||
| 7 | import GameCategory from '@components/GameCategory'; | 7 | import GameCategory from '@components/GameCategory'; |
| 8 | 8 | ||
| @@ -18,12 +18,12 @@ const GameEntry: React.FC<GameEntryProps> = ({ game }) => { | |||
| 18 | }, [game.category_portals]); | 18 | }, [game.category_portals]); |
| 19 | 19 | ||
| 20 | return ( | 20 | return ( |
| 21 | <Link to={"/games/" + game.id}><div className='games-page-item'> | 21 | <Link to={"/games/" + game.id}><div className={games.game}> |
| 22 | <div className='games-page-item-header'> | 22 | <div className={games.header}> |
| 23 | <div style={{ backgroundImage: `url(${game.image})` }} className='games-page-item-header-img'></div> | 23 | <div style={{ backgroundImage: `url(${game.image})` }}></div> |
| 24 | <span><b>{game.name}</b></span> | 24 | <span><b>{game.name}</b></span> |
| 25 | </div> | 25 | </div> |
| 26 | <div id={game.id as any as string} className='games-page-item-body'> | 26 | <div id={game.id as any as string} className={games.infoBlockContainer}> |
| 27 | {catInfo.map((cat, index) => { | 27 | {catInfo.map((cat, index) => { |
| 28 | return <GameCategory cat={cat} game={game} key={index}></GameCategory> | 28 | return <GameCategory cat={cat} game={game} key={index}></GameCategory> |
| 29 | })} | 29 | })} |
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'; | |||
| 5 | import { UserProfile } from '@customTypes/Profile'; | 5 | import { UserProfile } from '@customTypes/Profile'; |
| 6 | import { API } from '@api/Api'; | 6 | import { API } from '@api/Api'; |
| 7 | import "@css/Login.css"; | 7 | import "@css/Login.css"; |
| 8 | import { Button, Buttons } from "@customTypes/Sidebar"; | ||
| 9 | import btn from "@css/Button.module.css"; | ||
| 8 | 10 | ||
| 9 | interface LoginProps { | 11 | interface 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 | ||
| 15 | const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { | 21 | const 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 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | 2 | ||
| 3 | import btn from "@css/Button.module.css" | ||
| 3 | import "@css/Dialog.css" | 4 | import "@css/Dialog.css" |
| 4 | 5 | ||
| 5 | interface MessageDialogProps { | 6 | interface 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 @@ | |||
| 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 | |||
| 4 | import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images'; | 3 | import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images'; |
| 5 | import Login from '@components/Login'; | ||
| 6 | import { UserProfile } from '@customTypes/Profile'; | 4 | import { UserProfile } from '@customTypes/Profile'; |
| 7 | import { Search } from '@customTypes/Search'; | 5 | import sidebar from "@css/Sidebar.module.css"; |
| 6 | import { Button, Buttons } from "@customTypes/Sidebar"; | ||
| 7 | import btn from "@css/Button.module.css"; | ||
| 8 | import { abort } from "process"; | ||
| 9 | import Login from "@components/Login"; | ||
| 8 | import { API } from '@api/Api'; | 10 | import { API } from '@api/Api'; |
| 9 | import "@css/Sidebar.css"; | 11 | import inp from "@css/Input.module.css"; |
| 12 | import { Search } from '@customTypes/Search'; | ||
| 10 | 13 | ||
| 11 | interface SidebarProps { | 14 | interface 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 | ||
| 18 | const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUploadRun }) => { | 21 | const 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 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 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 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 Rules</span></button> | ||
| 166 | </Link> | ||
| 167 | |||
| 168 | <Link to="/about" tabIndex={-1}> | ||
| 169 | <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About 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 | ||
| 202 | export default Sidebar; | 148 | export 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 @@ | |||
| 1 | import React, { useRef } from 'react'; | ||
| 2 | import { Link, useLocation } from 'react-router-dom'; | ||
| 3 | |||
| 4 | import btn from "@css/Button.module.css"; | ||
| 5 | import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images'; | ||
| 6 | import Login from '@components/Login'; | ||
| 7 | import { UserProfile } from '@customTypes/Profile'; | ||
| 8 | import { Search } from '@customTypes/Search'; | ||
| 9 | import { API } from '@api/Api'; | ||
| 10 | import "@css/Sidebar.css"; | ||
| 11 | |||
| 12 | interface 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 | |||
| 19 | const 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 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 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 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 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 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 | |||
| 210 | export default Sidebar; | ||
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx index c02fdb8..971a747 100644 --- a/frontend/src/components/UploadRunDialog.tsx +++ b/frontend/src/components/UploadRunDialog.tsx | |||
| @@ -2,6 +2,7 @@ import React from 'react'; | |||
| 2 | import { UploadRunContent } from '@customTypes/Content'; | 2 | import { UploadRunContent } from '@customTypes/Content'; |
| 3 | import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from '@nekz/sdp'; | 3 | import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from '@nekz/sdp'; |
| 4 | 4 | ||
| 5 | import btn from "@css/Button.module.css"; | ||
| 5 | import '@css/UploadRunDialog.css'; | 6 | import '@css/UploadRunDialog.css'; |
| 6 | import { Game } from '@customTypes/Game'; | 7 | import { Game } from '@customTypes/Game'; |
| 7 | import { API } from '@api/Api'; | 8 | import { API } from '@api/Api'; |
| @@ -226,7 +227,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 226 | <span>Drag and drop</span> | 227 | <span>Drag and drop</span> |
| 227 | <div> | 228 | <div> |
| 228 | <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> | 229 | <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> |
| 229 | <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> | 230 | <button className={btn.default}>Upload</button> |
| 230 | </div> | 231 | </div> |
| 231 | </div> | 232 | </div> |
| 232 | : null} | 233 | : null} |
| @@ -242,11 +243,8 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 242 | <div onClick={() => { _handle_file_click(false) }} onDragOver={(e) => { _handle_drag_over(e, false) }} onDrop={(e) => { _handle_drop(e, false) }} onDragLeave={(e) => { _handle_drag_leave(e, false) }} className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}> | 243 | <div onClick={() => { _handle_file_click(false) }} onDragOver={(e) => { _handle_drag_over(e, false) }} onDrop={(e) => { _handle_drop(e, false) }} onDragLeave={(e) => { _handle_drag_leave(e, false) }} className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}> |
| 243 | <input ref={fileInputRefPartner} type="file" name="partner_demo" id="partner_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, false)} /> {!uploadRunContent.partner_demo ? | 244 | <input ref={fileInputRefPartner} type="file" name="partner_demo" id="partner_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, false)} /> {!uploadRunContent.partner_demo ? |
| 244 | <div> | 245 | <div> |
| 245 | <span>Drag and drop</span> | 246 | <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> |
| 246 | <div> | 247 | <button className={btn.default}>Upload</button> |
| 247 | <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> | ||
| 248 | <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> | ||
| 249 | </div> | ||
| 250 | </div> | 248 | </div> |
| 251 | : null} | 249 | : null} |
| 252 | 250 | ||
| @@ -265,9 +263,9 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 265 | ) | 263 | ) |
| 266 | } | 264 | } |
| 267 | </div> | 265 | </div> |
| 268 | <div className='upload-run-buttons-container'> | 266 | <div className='upload-run-buttons-container'> |
| 269 | <button onClick={_upload_run}>Submit</button> | 267 | <button className={`${btn.defaultWide}`} onClick={_upload_run}>Submit</button> |
| 270 | <button onClick={() => { | 268 | <button className={`${btn.defaultWide}`} onClick={() => { |
| 271 | onClose(false); | 269 | onClose(false); |
| 272 | setUploadRunContent({ | 270 | setUploadRunContent({ |
| 273 | host_demo: null, | 271 | host_demo: null, |
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/Games.module.css b/frontend/src/css/Games.module.css new file mode 100644 index 0000000..4c598cd --- /dev/null +++ b/frontend/src/css/Games.module.css | |||
| @@ -0,0 +1,61 @@ | |||
| 1 | .content { | ||
| 2 | position: relative; | ||
| 3 | display: flex; | ||
| 4 | flex-direction: column; | ||
| 5 | width: 100%; | ||
| 6 | gap: 24px; | ||
| 7 | margin-top: 24px; | ||
| 8 | } | ||
| 9 | |||
| 10 | .content a { | ||
| 11 | position: relative; | ||
| 12 | width: 100%; | ||
| 13 | } | ||
| 14 | |||
| 15 | .game { | ||
| 16 | display: flex; | ||
| 17 | width: 100%; | ||
| 18 | flex-direction: column; | ||
| 19 | overflow: hidden; | ||
| 20 | border-radius: 24px; | ||
| 21 | background-color: var(--primary-dark); | ||
| 22 | } | ||
| 23 | |||
| 24 | .header { | ||
| 25 | display: flex; | ||
| 26 | overflow: hidden; | ||
| 27 | position: relative; | ||
| 28 | flex: 1; | ||
| 29 | } | ||
| 30 | |||
| 31 | .header span { | ||
| 32 | position: relative; | ||
| 33 | z-index: 2; | ||
| 34 | display: flex; | ||
| 35 | text-align: center; | ||
| 36 | justify-content: center; | ||
| 37 | width: 100%; | ||
| 38 | padding: 24px 0px; | ||
| 39 | text-wrap: nowrap; | ||
| 40 | } | ||
| 41 | |||
| 42 | .header div { | ||
| 43 | position: absolute; | ||
| 44 | top: 0; | ||
| 45 | left: 0; | ||
| 46 | width: 100%; | ||
| 47 | height: 100%; | ||
| 48 | z-index: 1; | ||
| 49 | background-size: cover; | ||
| 50 | filter: blur(4px); | ||
| 51 | } | ||
| 52 | |||
| 53 | .infoBlockContainer { | ||
| 54 | flex: 1; | ||
| 55 | display: flex; | ||
| 56 | gap: 12px; | ||
| 57 | margin: 12px; | ||
| 58 | align-items: center; | ||
| 59 | justify-content: center; | ||
| 60 | height: 50%; | ||
| 61 | } | ||
diff --git a/frontend/src/css/Info.module.css b/frontend/src/css/Info.module.css new file mode 100644 index 0000000..144346e --- /dev/null +++ b/frontend/src/css/Info.module.css | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | .infoBlock { | ||
| 2 | background-color: var(--primary); | ||
| 3 | display: flex; | ||
| 4 | width: 100%; | ||
| 5 | border-radius: 18px; | ||
| 6 | text-align: center; | ||
| 7 | justify-content: center; | ||
| 8 | padding: 4px 0px; | ||
| 9 | text-wrap: nowrap; | ||
| 10 | } | ||
| 11 | |||
| 12 | .infoBlock > div > span:nth-child(1) { | ||
| 13 | margin-top: 0px; | ||
| 14 | font-size: 26px; | ||
| 15 | } | ||
| 16 | |||
| 17 | .infoBlock > div > span:nth-child(3) { | ||
| 18 | font-size: 50px; | ||
| 19 | font-family: BarlowCondensed-Bold; | ||
| 20 | } | ||
| 21 | |||
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..356b062 --- /dev/null +++ b/frontend/src/css/Sidebar.module.css | |||
| @@ -0,0 +1,164 @@ | |||
| 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 b { | ||
| 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 | /* this can be improved */ | ||
| 113 | .result { | ||
| 114 | margin: 10px 6px 0 6px; | ||
| 115 | height: 80px; | ||
| 116 | width: 100%; | ||
| 117 | max-width: 285px; | ||
| 118 | animation: result_in 0.2s ease; | ||
| 119 | animation-fill-mode: backwards; | ||
| 120 | overflow: hidden; | ||
| 121 | |||
| 122 | border-radius: 20px; | ||
| 123 | text-align: center; | ||
| 124 | |||
| 125 | display: grid; | ||
| 126 | |||
| 127 | border: 0; | ||
| 128 | transition: background-color .1s; | ||
| 129 | background-color: #2b2e46; | ||
| 130 | grid-template-rows: 20% 20% 60%; | ||
| 131 | width: calc(100% - 15px); | ||
| 132 | } | ||
| 133 | .result>span{ | ||
| 134 | color: #888; | ||
| 135 | font-size: 16px; | ||
| 136 | font-family: BarlowSemiCondensed-Regular; | ||
| 137 | } | ||
| 138 | .result>span:nth-child(3), .result.player span{ | ||
| 139 | font-size: 30px; | ||
| 140 | color: #CDCFDF; | ||
| 141 | } | ||
| 142 | |||
| 143 | .result.player img { | ||
| 144 | height: 80px; | ||
| 145 | } | ||
| 146 | |||
| 147 | .result.player span { | ||
| 148 | display: flex; | ||
| 149 | text-align: left; | ||
| 150 | margin-left: 90px; | ||
| 151 | width: fit-content; | ||
| 152 | } | ||
| 153 | |||
| 154 | @keyframes result_in { | ||
| 155 | 0% { | ||
| 156 | opacity: 0; | ||
| 157 | transform: translateY(20px); | ||
| 158 | } | ||
| 159 | 100% { | ||
| 160 | opacity: 1; | ||
| 161 | transform: translateY(0px); | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
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 | ||
| 99 | button, 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 | ||
| 114 | button: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 a8b7826..b7bd534 100644 --- a/frontend/src/pages/About.tsx +++ b/frontend/src/pages/About.tsx | |||
| @@ -28,12 +28,12 @@ const About: React.FC = () => { | |||
| 28 | 28 | ||
| 29 | 29 | ||
| 30 | return ( | 30 | return ( |
| 31 | <div id="about"> | 31 | <main> |
| 32 | <Helmet> | 32 | <Helmet> |
| 33 | <title>LPHUB | About</title> | 33 | <title>LPHUB | About</title> |
| 34 | </Helmet> | 34 | </Helmet> |
| 35 | <ReactMarkdown>{aboutText}</ReactMarkdown> | 35 | <ReactMarkdown>{aboutText}</ReactMarkdown> |
| 36 | </div> | 36 | </main> |
| 37 | ); | 37 | ); |
| 38 | }; | 38 | }; |
| 39 | 39 | ||
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx index 15cc891..5e0d5bf 100644 --- a/frontend/src/pages/Games.tsx +++ b/frontend/src/pages/Games.tsx | |||
| @@ -3,43 +3,26 @@ import { Helmet } from 'react-helmet'; | |||
| 3 | 3 | ||
| 4 | import GameEntry from '@components/GameEntry'; | 4 | import GameEntry from '@components/GameEntry'; |
| 5 | import { Game } from '@customTypes/Game'; | 5 | import { Game } from '@customTypes/Game'; |
| 6 | import "@css/Maps.css" | 6 | import gamesCSS from "@css/Games.module.css"; |
| 7 | 7 | ||
| 8 | interface GamesProps { | 8 | interface GamesProps { |
| 9 | games: Game[]; | 9 | games: Game[]; |
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | const Games: React.FC<GamesProps> = ({ games }) => { | 12 | const Games: React.FC<GamesProps> = ({ games }) => { |
| 13 | |||
| 14 | const _page_load = () => { | ||
| 15 | const loaders = document.querySelectorAll(".loader"); | ||
| 16 | loaders.forEach((loader) => { | ||
| 17 | (loader as HTMLElement).style.display = "none"; | ||
| 18 | }); | ||
| 19 | } | ||
| 20 | |||
| 21 | React.useEffect(() => { | ||
| 22 | document.querySelectorAll(".games-page-item-body").forEach((game, index) => { | ||
| 23 | game.innerHTML = ""; | ||
| 24 | }); | ||
| 25 | _page_load(); | ||
| 26 | }, []); | ||
| 27 | |||
| 28 | return ( | 13 | return ( |
| 29 | <div className='games-page'> | 14 | <main> |
| 30 | <Helmet> | 15 | <Helmet> |
| 31 | <title>LPHUB | Games</title> | 16 | <title>LPHUB | Games</title> |
| 32 | </Helmet> | 17 | </Helmet> |
| 33 | <section> | 18 | <section> |
| 34 | <div className='games-page-content'> | 19 | <div className={gamesCSS.content}> |
| 35 | <div className='games-page-item-content'> | 20 | {games.map((game, index) => ( |
| 36 | {games.map((game, index) => ( | 21 | <GameEntry game={game} key={index} /> |
| 37 | <GameEntry game={game} key={index} /> | 22 | ))} |
| 38 | ))} | ||
| 39 | </div> | ||
| 40 | </div> | 23 | </div> |
| 41 | </section> | 24 | </section> |
| 42 | </div> | 25 | </main> |
| 43 | ); | 26 | ); |
| 44 | }; | 27 | }; |
| 45 | 28 | ||
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx index 76f9a52..b9e17f7 100644 --- a/frontend/src/pages/Maplist.tsx +++ b/frontend/src/pages/Maplist.tsx | |||
| @@ -88,7 +88,13 @@ const Maplist: React.FC = () => { | |||
| 88 | } | 88 | } |
| 89 | }, [gameChapters]) | 89 | }, [gameChapters]) |
| 90 | 90 | ||
| 91 | 91 | useEffect(() => { | |
| 92 | const queryParams = new URLSearchParams(location.search); | ||
| 93 | const cat = queryParams.get("cat"); | ||
| 94 | if (cat != null) { | ||
| 95 | setCatNum(parseFloat(cat) - 1); | ||
| 96 | } | ||
| 97 | }, [location]) | ||
| 92 | 98 | ||
| 93 | return ( | 99 | return ( |
| 94 | <main> | 100 | <main> |
| @@ -121,7 +127,9 @@ const Maplist: React.FC = () => { | |||
| 121 | )?.portal_count | 127 | )?.portal_count |
| 122 | } | 128 | } |
| 123 | </h2> | 129 | </h2> |
| 124 | <h3>portals</h3> | 130 | <h3>{game?.category_portals.find( |
| 131 | (obj) => obj.category.id === catNum + 1)!.portal_count == 1 ? "portal" : "portals" | ||
| 132 | }</h3> | ||
| 125 | </div> | 133 | </div> |
| 126 | <div className="game-header-categories"> | 134 | <div className="game-header-categories"> |
| 127 | {game?.category_portals.map((cat, index) => ( | 135 | {game?.category_portals.map((cat, index) => ( |
| @@ -160,7 +168,7 @@ const Maplist: React.FC = () => { | |||
| 160 | <span>{map.is_disabled ? map.category_portals[0].portal_count : map.category_portals.find( | 168 | <span>{map.is_disabled ? map.category_portals[0].portal_count : map.category_portals.find( |
| 161 | (obj) => obj.category.id === catNum + 1 | 169 | (obj) => obj.category.id === catNum + 1 |
| 162 | )?.portal_count}</span> | 170 | )?.portal_count}</span> |
| 163 | <span>portals</span> | 171 | <span>{map.category_portals.find((obj) => obj.category.id === catNum + 1)?.portal_count == 1 ? "portal" : "portals"}</span> |
| 164 | </div> | 172 | </div> |
| 165 | </div> | 173 | </div> |
| 166 | <div className="difficulty-bar"> | 174 | <div className="difficulty-bar"> |
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 48233bf..ee56999 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx | |||
| @@ -109,7 +109,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 109 | }; | 109 | }; |
| 110 | 110 | ||
| 111 | return ( | 111 | return ( |
| 112 | <div> | 112 | <div style={{position: "absolute", width: "calc(100% - 50px)", left: "350px"}}> |
| 113 | <Helmet> | 113 | <Helmet> |
| 114 | <title>LPHUB | {profile.user_name}</title> | 114 | <title>LPHUB | {profile.user_name}</title> |
| 115 | <meta name="description" content={profile.user_name} /> | 115 | <meta name="description" content={profile.user_name} /> |
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 @@ | |||
| 1 | import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images'; | ||
| 2 | |||
| 3 | export interface Button { | ||
| 4 | img: string; | ||
| 5 | text: string; | ||
| 6 | url: string; | ||
| 7 | } | ||
| 8 | |||
| 9 | export interface Buttons { | ||
| 10 | top: Button[]; | ||
| 11 | bottom: Button[]; | ||
| 12 | } | ||