Implement new header design (so far)

This commit is contained in:
Justin Edmund 2023-01-27 20:30:39 -08:00
parent ceb87a8016
commit fa1bf57964
5 changed files with 278 additions and 223 deletions

View file

@ -286,9 +286,9 @@ const AccountModal = (props: Props) => {
return (
<Dialog open={open} onOpenChange={openChange}>
<DialogTrigger asChild>
<li className="MenuItem">
<div className="MenuItem">
<span>{t('menu.settings')}</span>
</li>
</div>
</DialogTrigger>
<DialogContent
className="Account"

View file

@ -138,6 +138,10 @@
display: flex;
&.Arrow {
margin-top: $unit-half;
}
svg {
fill: var(--button-text);
height: $dimension;

View file

@ -5,11 +5,18 @@
justify-content: space-between;
width: 100%;
#Right > div {
section {
display: flex;
gap: $unit;
}
img {
$diameter: 32px;
border-radius: calc($diameter / 2);
height: $diameter;
width: $diameter;
}
#DropdownWrapper {
display: inline-block;
padding-bottom: $unit;
@ -20,7 +27,7 @@
}
&:hover {
padding-right: $unit-4x;
// padding-right: $unit-4x;
.Button {
background: var(--button-bg-hover);

View file

@ -3,12 +3,13 @@ import { useSnapshot } from 'valtio'
import { deleteCookie } from 'cookies-next'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import clonedeep from 'lodash.clonedeep'
import api from '~utils/api'
import { accountState, initialAccountState } from '~utils/accountState'
import { appState, initialAppState } from '~utils/appState'
import capitalizeFirstLetter from '~utils/capitalizeFirstLetter'
import Button from '~components/Button'
import HeaderMenu from '~components/HeaderMenu'
@ -16,10 +17,22 @@ import HeaderMenu from '~components/HeaderMenu'
import AddIcon from '~public/icons/Add.svg'
import LinkIcon from '~public/icons/Link.svg'
import MenuIcon from '~public/icons/Menu.svg'
import ArrowIcon from '~public/icons/Arrow.svg'
import SaveIcon from '~public/icons/Save.svg'
import classNames from 'classnames'
import './index.scss'
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
} from '~components/DropdownMenuContent'
import Link from 'next/link'
import LoginModal from '~components/LoginModal'
import SignupModal from '~components/SignupModal'
import AccountModal from '~components/AccountModal'
const Header = () => {
// Localization
@ -30,19 +43,40 @@ const Header = () => {
// State management
const [open, setOpen] = useState(false)
const [leftMenuOpen, setLeftMenuOpen] = useState(false)
const [rightMenuOpen, setRightMenuOpen] = useState(false)
// Snapshots
const { account } = useSnapshot(accountState)
const { party } = useSnapshot(appState)
function menuButtonClicked() {
setOpen(!open)
function handleLeftMenuButtonClicked() {
setLeftMenuOpen(!leftMenuOpen)
}
function handleRightMenuButtonClicked() {
setRightMenuOpen(!rightMenuOpen)
}
function onClickOutsideMenu() {
setOpen(false)
}
function handleLeftMenuOpenChange(open: boolean) {
setLeftMenuOpen(open)
}
function handleRightMenuOpenChange(open: boolean) {
setRightMenuOpen(open)
}
function closeLeftMenu() {
setLeftMenuOpen(false)
}
function closeRightMenu() {
setRightMenuOpen(false)
}
function copyToClipboard() {
const el = document.createElement('input')
el.value = window.location.href
@ -69,6 +103,10 @@ const Header = () => {
}
function logout() {
// Close menu
closeRightMenu()
// Delete cookies
deleteCookie('account')
deleteCookie('user')
@ -107,7 +145,7 @@ const Header = () => {
if (router.route === '/p/[party]')
return (
<Button
accessoryIcon={<LinkIcon className="stroke" />}
leftAccessoryIcon={<LinkIcon className="stroke" />}
blended={true}
text={t('buttons.copy')}
onClick={copyToClipboard}
@ -115,24 +153,39 @@ const Header = () => {
)
}
const leftNav = () => {
const title = () => {
let title = ''
let hasAccessory = false
const path = router.asPath.split('/')[1]
console.log(router.asPath.split('/'))
if (path === 'p') {
hasAccessory = true
if (appState.party && appState.party.name) {
title = appState.party.name
} else {
title = t('no_title')
}
} else if (['weapons', 'summons', 'characters', 'new', ''].includes(path)) {
title = t('new_party')
} else if (
['about', 'updates', 'roadmap', 'saved', 'teams'].includes(path)
) {
title = capitalizeFirstLetter(path)
} else {
title = path
}
return (
<div id="DropdownWrapper">
<Button
accessoryIcon={<MenuIcon />}
className={classNames({ Active: open })}
blended={true}
text={t('buttons.menu')}
onClick={menuButtonClicked}
/>
<HeaderMenu
authenticated={account.authorized}
open={open}
username={account.user?.username}
onClickOutside={onClickOutsideMenu}
logout={logout}
/>
</div>
<Button
blended={true}
rightAccessoryIcon={
path === 'p' && hasAccessory ? (
<LinkIcon className="stroke" />
) : undefined
}
text={title}
/>
)
}
@ -140,7 +193,7 @@ const Header = () => {
if (party.favorited)
return (
<Button
accessoryIcon={<SaveIcon />}
leftAccessoryIcon={<SaveIcon />}
blended={true}
text="Saved"
onClick={toggleFavorite}
@ -149,7 +202,7 @@ const Header = () => {
else
return (
<Button
accessoryIcon={<SaveIcon />}
leftAccessoryIcon={<SaveIcon />}
blended={true}
text="Save"
onClick={toggleFavorite}
@ -157,9 +210,56 @@ const Header = () => {
)
}
const rightNav = () => {
const image = () => {
let image
const user = accountState.account.user
if (accountState.account.authorized && user) {
image = (
<img
alt={user.username}
className={`profile ${user.element}`}
srcSet={`/profile/${user.picture}.png,
/profile/${user.picture}@2x.png 2x`}
src={`/profile/${user.picture}.png`}
/>
)
} else {
image = <img alt={t('no_user')} className="profile placeholder" />
}
return image
}
const left = () => {
return (
<div>
<section>
<div id="DropdownWrapper">
<DropdownMenu
open={leftMenuOpen}
onOpenChange={handleLeftMenuOpenChange}
>
<DropdownMenuTrigger asChild>
<Button
leftAccessoryIcon={<MenuIcon />}
className={classNames({ Active: leftMenuOpen })}
blended={true}
onClick={handleLeftMenuButtonClicked}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="Left">
{leftMenuItems()}
</DropdownMenuContent>
</DropdownMenu>
</div>
{title()}
</section>
)
}
const right = () => {
return (
<section>
{router.route === '/p/[party]' &&
account.user &&
(!party.user || party.user.id !== account.user.id)
@ -168,20 +268,151 @@ const Header = () => {
{copyButton()}
<Button
accessoryIcon={<AddIcon className="Add" />}
{/* <Button
leftAccessoryIcon={<AddIcon className="Add" />}
blended={true}
text={t('buttons.new')}
onClick={newParty}
/>
</div>
/> */}
<DropdownMenu
open={rightMenuOpen}
onOpenChange={handleRightMenuOpenChange}
>
<DropdownMenuTrigger asChild>
<Button
className={classNames({ Active: rightMenuOpen })}
leftAccessoryIcon={image()}
rightAccessoryIcon={<ArrowIcon />}
rightAccessoryClassName="Arrow"
onClick={handleRightMenuButtonClicked}
blended={true}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="Right">
{rightMenuItems()}
</DropdownMenuContent>
</DropdownMenu>
</section>
)
}
const leftMenuItems = () => {
return (
<>
{accountState.account.authorized && accountState.account.user ? (
<>
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuItem className="MenuItem" onClick={closeRightMenu}>
<Link
href={`/${accountState.account.user.username}` || ''}
passHref
>
Your profile
</Link>
</DropdownMenuItem>
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
<Link href={`/saved` || ''}>{t('menu.saved')}</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
</>
) : (
''
)}
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
<Link href="/teams">{t('menu.teams')}</Link>
</DropdownMenuItem>
<DropdownMenuItem className="MenuItem">
<div>
<span>{t('menu.guides')}</span>
<i className="tag">{t('coming_soon')}</i>
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
<a href="/about" target="_blank">
{t('about.segmented_control.about')}
</a>
</DropdownMenuItem>
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
<a href="/updates" target="_blank">
{t('about.segmented_control.updates')}
</a>
</DropdownMenuItem>
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
<a href="/roadmap" target="_blank">
{t('about.segmented_control.roadmap')}
</a>
</DropdownMenuItem>
</DropdownMenuGroup>
</>
)
}
const rightMenuItems = () => {
let items
const account = accountState.account
if (account.authorized && account.user) {
items = (
<>
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuItem className="MenuItem" onClick={closeRightMenu}>
<Link href={`/new` || ''} passHref>
New party
</Link>
</DropdownMenuItem>
<DropdownMenuItem className="MenuItem" onClick={closeRightMenu}>
<Link href={`/${account.user.username}` || ''} passHref>
Your profile
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuItem
className="MenuItem"
onClick={closeRightMenu}
asChild
>
<AccountModal
username={account.user.username}
picture={account.user.picture}
gender={account.user.gender}
language={account.user.language}
theme={account.user.theme}
/>
</DropdownMenuItem>
<DropdownMenuItem className="MenuItem" onClick={logout}>
<span>{t('menu.logout')}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</>
)
} else {
items = (
<>
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuItem className="MenuItem">New party</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup className="MenuGroup">
<LoginModal />
<SignupModal />
</DropdownMenuGroup>
</>
)
}
return items
}
return (
<nav id="Header">
<div id="Left">{leftNav()}</div>
<div id="Right">{rightNav()}</div>
{left()}
{right()}
</nav>
)
}

View file

@ -1,187 +0,0 @@
.Menu {
background: var(--menu-bg);
border-radius: 6px;
box-sizing: border-box;
display: none;
min-width: 220px;
position: absolute;
top: $unit-8x; // This shouldn't be hardcoded. How to calculate it?
// Also, add space that doesn't make the menu disappear if you move your mouse slowly
z-index: 10;
@include breakpoint(phone) {
left: $unit-2x;
right: $unit-2x;
}
}
.MenuItem {
color: var(--text-tertiary);
font-weight: $normal;
@include breakpoint(phone) {
cursor: pointer;
}
&:hover:not(.disabled) {
background: var(--menu-bg-item-hover);
color: var(--text-primary);
cursor: pointer;
a {
color: var(--text-primary);
&:hover {
text-decoration: none;
}
&:visited {
color: var(--text-primary);
}
}
@include breakpoint(phone) {
background: inherit;
color: inherit;
cursor: default;
a {
color: inherit;
}
}
}
&.profile > div {
padding: 6px 12px;
}
&.language {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit;
padding-right: $unit;
span {
flex-grow: 1;
}
.Switch {
$height: 24px;
background: $grey-60;
border-radius: calc($height / 2);
border: none;
position: relative;
width: 44px;
height: $height;
&:hover {
cursor: pointer;
}
.Thumb {
$diameter: 18px;
background: $grey-100;
border-radius: calc($diameter / 2);
display: block;
height: $diameter;
width: $diameter;
position: absolute;
top: 3px;
left: 3px;
z-index: 3;
&:hover {
cursor: pointer;
}
&[data-state='checked'] {
background: $grey-100;
left: 23px;
}
}
.left,
.right {
color: $grey-100;
font-size: 10px;
font-weight: $bold;
position: absolute;
z-index: 2;
}
.left {
top: 6px;
left: 6px;
}
.right {
top: 6px;
right: 5px;
}
}
}
a {
color: $grey-50;
&:hover {
text-decoration: none;
}
&:visited {
color: $grey-50;
}
}
& > a,
& > span {
display: block;
padding: 12px 12px;
}
& > div {
align-items: center;
display: flex;
flex-direction: row;
padding: 10px 12px;
&:hover {
i.tag {
background: var(--tag-bg);
color: var(--tag-text);
}
}
span {
flex-grow: 1;
}
img {
$diameter: 32px;
border-radius: calc($diameter / 2);
height: $diameter;
width: $diameter;
}
}
}
.MenuGroup {
border-bottom: 1px solid var(--menu-separator);
&:first-child .MenuItem:first-child:hover {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
&:last-child .MenuItem:last-child:hover {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
&:last-child {
border-bottom: none;
}
}