Add left nav (#4705)

* Automated translation update ( bf08a04cda )

* Add Layout component wip

* finish layout implementation

* convert header to left nav

* better nav item spacing

* return source files to original glory

* lint fix

* Remove height limit on login screen

* fix top spacing on 3-panel layout

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Brent Clark <brent@backboneiq.com>
This commit is contained in:
Brent Clark 2023-03-15 22:45:55 -05:00 committed by GitHub
parent cab5f29b57
commit cef99cba71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 280 additions and 201 deletions

View file

@ -33,4 +33,5 @@
.three-pane-layout .grid-main__bottom.fixedHeight { .three-pane-layout .grid-main__bottom.fixedHeight {
height: 50%; height: 50%;
overflow: visible; overflow: visible;
padding: 0 0 16px;
} }

View file

@ -1,5 +1,4 @@
// eslint-disable-next-line import { Component, CElement } from "react";
import React, { Component, CElement } from "react";
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import Hidden from '@mui/material/Hidden'; import Hidden from '@mui/material/Hidden';
@ -12,7 +11,7 @@ class ThreePaneLayout extends Component<ThreePaneLayoutProps> {
render() { render() {
return ( return (
<div className="three-pane-layout"> <div className="three-pane-layout">
<Grid container spacing={2} className="grid"> <Grid container rowSpacing={0} columnSpacing={2} className="grid">
<Grid item xs={12} md={9} lg={10} className="grid-main"> <Grid item xs={12} md={9} lg={10} className="grid-main">
<Grid item className={ <Grid item className={
'grid-main__top' 'grid-main__top'

View file

@ -2,7 +2,6 @@
export { default as Card } from './Card/Card'; export { default as Card } from './Card/Card';
export { default as CardDetails } from './CardDetails/CardDetails'; export { default as CardDetails } from './CardDetails/CardDetails';
export { default as CountryDropdown } from './CountryDropdown/CountryDropdown'; export { default as CountryDropdown } from './CountryDropdown/CountryDropdown';
export { default as Header } from './Header/Header';
export { default as InputField } from './InputField/InputField'; export { default as InputField } from './InputField/InputField';
export { default as InputAction } from './InputAction/InputAction'; export { default as InputAction } from './InputAction/InputAction';
export { default as KnownHosts } from './KnownHosts/KnownHosts'; export { default as KnownHosts } from './KnownHosts/KnownHosts';

View file

@ -11,6 +11,7 @@ import { UserDisplay, VirtualList, AuthGuard, LanguageDropdown } from 'component
import { AuthenticationService, SessionService } from 'api'; import { AuthenticationService, SessionService } from 'api';
import { ServerSelectors } from 'store'; import { ServerSelectors } from 'store';
import { User } from 'types'; import { User } from 'types';
import Layout from 'containers/Layout/Layout';
import AddToBuddies from './AddToBuddies'; import AddToBuddies from './AddToBuddies';
import AddToIgnore from './AddToIgnore'; import AddToIgnore from './AddToIgnore';
@ -33,7 +34,7 @@ const Account = (props: AccountProps) => {
}; };
return ( return (
<div className="account"> <Layout className="account">
<AuthGuard /> <AuthGuard />
<div className="account-column"> <div className="account-column">
<Paper className="account-list"> <Paper className="account-list">
@ -96,7 +97,7 @@ const Account = (props: AccountProps) => {
</div> </div>
</Paper> </Paper>
</div> </div>
</div> </Layout>
) )
} }

View file

@ -3,7 +3,6 @@ import { Provider } from 'react-redux';
import { MemoryRouter as Router } from 'react-router-dom'; import { MemoryRouter as Router } from 'react-router-dom';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
import { store } from 'store'; import { store } from 'store';
import { Header } from 'components';
import Routes from './AppShellRoutes'; import Routes from './AppShellRoutes';
import FeatureDetection from './FeatureDetection'; import FeatureDetection from './FeatureDetection';
@ -29,8 +28,6 @@ class AppShell extends Component {
<ToastProvider> <ToastProvider>
<div className="AppShell" onContextMenu={this.handleContextMenu}> <div className="AppShell" onContextMenu={this.handleContextMenu}>
<Router> <Router>
<Header />
<FeatureDetection /> <FeatureDetection />
<Routes /> <Routes />
</Router> </Router>

View file

@ -2,16 +2,17 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { AuthGuard } from 'components/index'; import { AuthGuard } from 'components/index';
import Layout from 'containers/Layout/Layout';
import './Decks.css'; import './Decks.css';
class Decks extends Component { class Decks extends Component {
render() { render() {
return ( return (
<div> <Layout>
<AuthGuard /> <AuthGuard />
<span>"Decks"</span> <span>"Decks"</span>
</div> </Layout>
) )
} }
} }

View file

@ -2,16 +2,17 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { AuthGuard } from 'components'; import { AuthGuard } from 'components';
import Layout from 'containers/Layout/Layout';
import './Game.css'; import './Game.css';
class Game extends Component { class Game extends Component {
render() { render() {
return ( return (
<div> <Layout>
<AuthGuard /> <AuthGuard />
<span>"Game"</span> <span>"Game"</span>
</div> </Layout>
) )
} }
} }

View file

@ -8,6 +8,7 @@ import Typography from '@mui/material/Typography';
import { Images } from 'images'; import { Images } from 'images';
import { ServerSelectors } from 'store'; import { ServerSelectors } from 'store';
import { RouteEnum } from 'types'; import { RouteEnum } from 'types';
import Layout from 'containers/Layout/Layout';
import './Initialize.css'; import './Initialize.css';
@ -35,6 +36,7 @@ const Initialize = ({ initialized }: InitializeProps) => {
return initialized return initialized
? <Navigate to={RouteEnum.LOGIN} /> ? <Navigate to={RouteEnum.LOGIN} />
: ( : (
<Layout>
<Root className={'Initialize ' + classes.root}> <Root className={'Initialize ' + classes.root}>
<div className='Initialize-content'> <div className='Initialize-content'>
<img src={Images.Logo} alt="logo" /> <img src={Images.Logo} alt="logo" />
@ -54,6 +56,7 @@ const Initialize = ({ initialized }: InitializeProps) => {
<div className="bottomBar Initialize-graphics__bar" /> <div className="bottomBar Initialize-graphics__bar" />
</div> </div>
</Root> </Root>
</Layout>
); );
} }

View file

@ -0,0 +1,31 @@
.layout {
height: 100%;
max-height: 100%;
width: 100%;
max-width: 100%;
display: flex;
flex-flow: row nowrap;
overflow: hidden;
}
.layout--no-height-limit {
height: initial;
max-height: initial;
}
.bottom-bar__container {
background: #555;
height: 50px;
width: 100%;
}
.page__body {
flex: 1;
max-height: calc(100% - 50px);
}
.page {
display: flex;
flex-flow: column;
width: 100%;
}

View file

@ -0,0 +1,39 @@
import LeftNav from './LeftNav';
import './Layout.css'
function Layout(props:LayoutProps) {
const { children, className, showNav = true, noHeightLimit = false } = props;
const containerClasses = ['layout']
if (noHeightLimit === true) {
containerClasses.push('layout--no-height-limit')
}
return (
<div className={containerClasses.join(" ")}>
{showNav && <LeftNav />}
<section className="page">
<div className={`page__body ${className}`}>
{children}
</div>
{showNav && <BottomBar />}
</section>
</div>
)
}
function BottomBar(props) {
return (
<div className="bottom-bar__container">
</div>
)
}
interface LayoutProps {
showNav?: boolean;
children: any;
className?: string;
noHeightLimit?: boolean
}
export default Layout;

View file

@ -1,31 +1,34 @@
.Header { .LeftNav__container {
background: #7033DB;
width: 100px;
min-width: 100px;
height: 100%;
} }
.Header__logo { .LeftNav__logo {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
padding: 16px 0;
} }
.Header__logo a { .LeftNav__logo a {
line-height: 1; line-height: 1;
} }
.Header__logo img { .LeftNav__logo img {
height: 32px; height: 32px;
} }
.Header-content { .LeftNav-content {
display: flex;
align-items: center;
width: 100%;
color: white; color: white;
} }
.Header-serverDetails { .LeftNav-serverDetails {
font-size: 12px; font-size: 12px;
} }
.Header-server__indicator { .LeftNav-server__indicator {
display: inline-block; display: inline-block;
height: 12px; height: 12px;
width: 12px; width: 12px;
@ -35,46 +38,43 @@
margin-left: 10px; margin-left: 10px;
} }
.Header-nav { .LeftNav-nav {
width: 100%;
display: flex;
justify-content: space-between;
} }
.Header-nav__links { .LeftNav-nav__links {
width: 100%;
display: flex; display: flex;
padding-left: 50px; flex-flow: column;
align-items: center; align-items: center;
gap: 16px;
} }
.Header-nav__link { .LeftNav-nav__link {
position: relative; position: relative;
height: 100%; height: 100%;
} }
.Header-nav__link:hover { .LeftNav-nav__link:hover {
background: rgba(0, 0, 0, .125); background: rgba(0, 0, 0, .125);
} }
.Header-nav__link:hover .Header-nav__link-menu { .LeftNav-nav__link:hover .LeftNav-nav__link-menu {
display: block; display: block;
} }
.Header-nav__link-btn { .LeftNav-nav__link-btn {
display: flex; display: flex;
height: 100%; height: 100%;
width: 100%; width: 100%;
align-items: center; align-items: center;
padding: 5px 10px; padding: 5px 20px;
font-weight: bold; font-weight: bold;
} }
.Header-nav__link-btn__icon { .LeftNav-nav__link-btn__icon {
margin-left: 5px; margin-left: 5px;
} }
.Header-nav__link-menu { .LeftNav-nav__link-menu {
display: none; display: none;
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@ -82,13 +82,14 @@
min-width: 150px; min-width: 150px;
background: #3f51b5; background: #3f51b5;
box-shadow: 1px 1px 2px 0px black; box-shadow: 1px 1px 2px 0px black;
z-index: 1;
} }
.Header-nav__link-menu__item { .LeftNav-nav__link-menu__item {
padding: 0 !important; padding: 0 !important;
} }
.Header-nav__link-menu__btn { .LeftNav-nav__link-menu__btn {
padding: 6px 16px; padding: 6px 16px;
width: 100%; width: 100%;
color: white; color: white;
@ -96,15 +97,16 @@
justify-content: space-between; justify-content: space-between;
} }
.Header-nav__actions { .LeftNav-nav__actions {
display: flex; display: flex;
justify-content: center;
} }
.Header-nav__action { .LeftNav-nav__action {
} }
.Header-nav__action button { .LeftNav-nav__action button {
color: white; color: white;
} }
@ -124,4 +126,3 @@
.temp-chip > div { .temp-chip > div {
cursor: inherit; cursor: inherit;
} }

View file

@ -1,11 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { NavLink, useNavigate, generatePath } from 'react-router-dom'; import { NavLink, useNavigate, generatePath } from 'react-router-dom';
import AppBar from '@mui/material/AppBar';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Menu from '@mui/material/Menu'; import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import Toolbar from '@mui/material/Toolbar';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import MailOutlineRoundedIcon from '@mui/icons-material/MailOutline'; import MailOutlineRoundedIcon from '@mui/icons-material/MailOutline';
@ -18,11 +16,11 @@ import { Images } from 'images';
import { RoomsSelectors, ServerSelectors } from 'store'; import { RoomsSelectors, ServerSelectors } from 'store';
import { Room, RouteEnum, User } from 'types'; import { Room, RouteEnum, User } from 'types';
import './Header.css'; import './LeftNav.css';
const Header = ({ joinedRooms, serverState, user }: HeaderProps) => { const LeftNav = ({ joinedRooms, serverState, user }: LeftNavProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [state, setState] = useState<HeaderState>({ const [state, setState] = useState<LeftNavState>({
anchorEl: null, anchorEl: null,
showCardImportDialog: false, showCardImportDialog: false,
options: [], options: [],
@ -73,23 +71,23 @@ const Header = ({ joinedRooms, serverState, user }: HeaderProps) => {
} }
return ( return (
<AppBar className="Header" position="static"> <div className="LeftNav__container">
<Toolbar variant="dense"> <div>
<div className="Header__logo"> <div className="LeftNav__logo">
<NavLink to={RouteEnum.SERVER}> <NavLink to={RouteEnum.SERVER}>
<img src={Images.Logo} alt="logo" /> <img src={Images.Logo} alt="logo" />
</NavLink> </NavLink>
{ AuthenticationService.isConnected(serverState) && ( { AuthenticationService.isConnected(serverState) && (
<span className="Header-server__indicator"></span> <span className="LeftNav-server__indicator"></span>
) } ) }
</div> </div>
{ AuthenticationService.isConnected(serverState) && ( { AuthenticationService.isConnected(serverState) && (
<div className="Header-content"> <div className="LeftNav-content">
<nav className="Header-nav"> <nav className="LeftNav-nav">
<nav className="Header-nav__links"> <nav className="LeftNav-nav__links">
<div className="Header-nav__link"> <div className="LeftNav-nav__link">
<NavLink <NavLink
className="Header-nav__link-btn" className="LeftNav-nav__link-btn"
to={ to={
joinedRooms.length joinedRooms.length
? generatePath(RouteEnum.ROOM, { roomId: joinedRooms[0].roomId.toString() }) ? generatePath(RouteEnum.ROOM, { roomId: joinedRooms[0].roomId.toString() })
@ -97,42 +95,42 @@ const Header = ({ joinedRooms, serverState, user }: HeaderProps) => {
} }
> >
Rooms Rooms
<ArrowDropDownIcon className="Header-nav__link-btn__icon" fontSize="small" /> <ArrowDropDownIcon className="LeftNav-nav__link-btn__icon" fontSize="small" />
</NavLink> </NavLink>
<div className="Header-nav__link-menu"> <div className="LeftNav-nav__link-menu">
{joinedRooms.map(({ name, roomId }) => ( {joinedRooms.map(({ name, roomId }) => (
<MenuItem className="Header-nav__link-menu__item" key={roomId}> <div className="LeftNav-nav__link-menu__item" key={roomId}>
<NavLink className="Header-nav__link-menu__btn" to={ generatePath(RouteEnum.ROOM, { roomId: roomId.toString() }) }> <NavLink className="LeftNav-nav__link-menu__btn" to={ generatePath(RouteEnum.ROOM, { roomId: roomId.toString() }) }>
{name} {name}
<IconButton size="small" edge="end" onClick={event => leaveRoom(event, roomId)}> <IconButton size="small" edge="end" onClick={event => leaveRoom(event, roomId)}>
<CloseIcon style={{ fontSize: 10, color: 'white' }} /> <CloseIcon style={{ fontSize: 10, color: 'white' }} />
</IconButton> </IconButton>
</NavLink> </NavLink>
</MenuItem> </div>
))} ))}
</div> </div>
</div> </div>
<div className="Header-nav__link"> <div className="LeftNav-nav__link">
<NavLink className="Header-nav__link-btn" to={ RouteEnum.GAME }> <NavLink className="LeftNav-nav__link-btn" to={ RouteEnum.GAME }>
Games Games
<ArrowDropDownIcon className="Header-nav__link-btn__icon" fontSize="small" /> <ArrowDropDownIcon className="LeftNav-nav__link-btn__icon" fontSize="small" />
</NavLink> </NavLink>
</div> </div>
<div className="Header-nav__link"> <div className="LeftNav-nav__link">
<NavLink className="Header-nav__link-btn" to={ RouteEnum.DECKS }> <NavLink className="LeftNav-nav__link-btn" to={ RouteEnum.DECKS }>
Decks Decks
<ArrowDropDownIcon className="Header-nav__link-btn__icon" fontSize="small" /> <ArrowDropDownIcon className="LeftNav-nav__link-btn__icon" fontSize="small" />
</NavLink> </NavLink>
</div> </div>
</nav> </nav>
<div className="Header-nav__actions"> <div className="LeftNav-nav__actions">
<div className="Header-nav__action"> <div className="LeftNav-nav__action">
<IconButton size="large"> <IconButton size="large">
<MailOutlineRoundedIcon style={{ color: 'inherit' }} /> <MailOutlineRoundedIcon style={{ color: 'inherit' }} />
</IconButton> </IconButton>
</div> </div>
<div className="Header-nav__action"> <div className="LeftNav-nav__action">
<IconButton onClick={handleMenuOpen} size="large"> <IconButton onClick={handleMenuOpen} size="large">
<MenuRoundedIcon style={{ color: 'inherit' }} /> <MenuRoundedIcon style={{ color: 'inherit' }} />
</IconButton> </IconButton>
@ -163,24 +161,25 @@ const Header = ({ joinedRooms, serverState, user }: HeaderProps) => {
</nav> </nav>
</div> </div>
) } ) }
</Toolbar> </div>
<CardImportDialog <CardImportDialog
isOpen={state.showCardImportDialog} isOpen={state.showCardImportDialog}
handleClose={closeImportCardWizard} handleClose={closeImportCardWizard}
></CardImportDialog> ></CardImportDialog>
</AppBar> </div>
); );
} }
interface HeaderProps { interface LeftNavProps {
serverState: number; serverState: number;
server: string; server: string;
user: User; user: User;
joinedRooms: Room[]; joinedRooms: Room[];
showNav?: boolean;
} }
interface HeaderState { interface LeftNavState {
anchorEl: Element; anchorEl: Element;
showCardImportDialog: boolean; showCardImportDialog: boolean;
options: string[]; options: string[];
@ -193,4 +192,4 @@ const mapStateToProps = state => ({
joinedRooms: RoomsSelectors.getJoinedRooms(state), joinedRooms: RoomsSelectors.getJoinedRooms(state),
}); });
export default connect(mapStateToProps)(Header); export default connect(mapStateToProps)(LeftNav);

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -16,6 +16,7 @@ import { Images } from 'images';
import { HostDTO, serverProps } from 'services'; import { HostDTO, serverProps } from 'services';
import { RouteEnum, WebSocketConnectOptions, getHostPort } from 'types'; import { RouteEnum, WebSocketConnectOptions, getHostPort } from 'types';
import { ServerSelectors, ServerTypes } from 'store'; import { ServerSelectors, ServerTypes } from 'store';
import Layout from 'containers/Layout/Layout';
import './Login.css'; import './Login.css';
import { useToast } from 'components/Toast'; import { useToast } from 'components/Toast';
@ -225,6 +226,7 @@ const Login = ({ state, description, connectOptions }: LoginProps) => {
}; };
return ( return (
<Layout showNav={false} noHeightLimit={true}>
<Root className={'login overflow-scroll ' + classes.root}> <Root className={'login overflow-scroll ' + classes.root}>
{ isConnected && <Navigate to={RouteEnum.SERVER} />} { isConnected && <Navigate to={RouteEnum.SERVER} />}
@ -339,6 +341,7 @@ const Login = ({ state, description, connectOptions }: LoginProps) => {
handleClose={closeActivateAccountDialog} handleClose={closeActivateAccountDialog}
/> />
</Root> </Root>
</Layout>
); );
} }

View file

@ -1,15 +1,16 @@
// eslint-disable-next-line // eslint-disable-next-line
import React, { Component } from "react"; import React, { Component } from "react";
import Layout from 'containers/Layout/Layout';
import { AuthGuard } from 'components'; import { AuthGuard } from 'components';
class Player extends Component { class Player extends Component {
render() { render() {
return ( return (
<div> <Layout>
<AuthGuard /> <AuthGuard />
<span>"Player"</span> <span>"Player"</span>
</div> </Layout>
) )
} }
} }

View file

@ -9,6 +9,7 @@ import { RoomsService } from 'api';
import { ScrollToBottomOnChanges, ThreePaneLayout, UserDisplay, VirtualList, AuthGuard } from 'components'; import { ScrollToBottomOnChanges, ThreePaneLayout, UserDisplay, VirtualList, AuthGuard } from 'components';
import { RoomsStateMessages, RoomsStateRooms, JoinedRooms, RoomsSelectors, RoomsTypes } from 'store'; import { RoomsStateMessages, RoomsStateRooms, JoinedRooms, RoomsSelectors, RoomsTypes } from 'store';
import { RouteEnum } from 'types'; import { RouteEnum } from 'types';
import Layout from 'containers/Layout/Layout';
import OpenGames from './OpenGames'; import OpenGames from './OpenGames';
import Messages from './Messages'; import Messages from './Messages';
@ -40,7 +41,7 @@ const Room = (props) => {
} }
return ( return (
<div className="room-view"> <Layout className="room-view">
<AuthGuard /> <AuthGuard />
<div className="room-view__main"> <div className="room-view__main">
@ -84,7 +85,7 @@ const Room = (props) => {
)} )}
/> />
</div> </div>
</div> </Layout>
); );
} }

View file

@ -11,6 +11,7 @@ import { useReduxEffect } from 'hooks';
import { RoomsSelectors, RoomsTypes, ServerSelectors } from 'store'; import { RoomsSelectors, RoomsTypes, ServerSelectors } from 'store';
import { Room, RouteEnum, User } from 'types'; import { Room, RouteEnum, User } from 'types';
import Rooms from './Rooms'; import Rooms from './Rooms';
import Layout from 'containers/Layout/Layout';
import './Server.css'; import './Server.css';
@ -23,7 +24,7 @@ const Server = ({ message, rooms, joinedRooms, users }: ServerProps) => {
}, RoomsTypes.JOIN_ROOM, []); }, RoomsTypes.JOIN_ROOM, []);
return ( return (
<div className="server-rooms"> <Layout className="server-rooms">
<AuthGuard /> <AuthGuard />
<ThreePaneLayout <ThreePaneLayout
@ -55,7 +56,7 @@ const Server = ({ message, rooms, joinedRooms, users }: ServerProps) => {
</Paper> </Paper>
)} )}
/> />
</div> </Layout>
); );
} }

View file

@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Layout from 'containers/Layout/Layout';
import './Unsupported.css'; import './Unsupported.css';
@ -9,7 +10,7 @@ const Unsupported = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className='Unsupported'> <Layout className='Unsupported'>
<Paper className='Unsupported-paper'> <Paper className='Unsupported-paper'>
<div className='Unsupported-paper__header'> <div className='Unsupported-paper__header'>
<Typography variant="h1">{ t('UnsupportedContainer.title') }</Typography> <Typography variant="h1">{ t('UnsupportedContainer.title') }</Typography>
@ -18,7 +19,7 @@ const Unsupported = () => {
<Typography variant="subtitle2">{ t('UnsupportedContainer.subtitle2') }</Typography> <Typography variant="subtitle2">{ t('UnsupportedContainer.subtitle2') }</Typography>
</Paper> </Paper>
</div> </Layout>
); );
}; };