feat: UI work on navbar responsivity (#244)
* feat: add useMediaQuery * feat: add menu icon * feat: add navbar responsivity * feat: add current path indication for navigation * feat: prevent screen zoom * refactor: move logo to navbar folder * refactor: move sidebar to single file * refactor: set icon names to kebab case * feat: add alpha color function * feat: add backdrop filter on navbar
This commit is contained in:
parent
cbe8b93594
commit
ae8ba51c84
|
|
@ -11,7 +11,10 @@
|
|||
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Minimal UI for Sites as NFTs. Fleek XYZ"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { AiOutlineTwitter } from '@react-icons/all-files/ai/AiOutlineTwitter';
|
|||
import { BiGitBranch } from '@react-icons/all-files/bi/BiGitBranch';
|
||||
import { BiSearch } from '@react-icons/all-files/bi/BiSearch';
|
||||
import { BsFillSquareFill } from '@react-icons/all-files/bs/BsFillSquareFill';
|
||||
import { FaBars } from '@react-icons/all-files/fa/FaBars';
|
||||
import { FaChevronRight } from '@react-icons/all-files/fa/FaChevronRight';
|
||||
import { FaExternalLinkAlt } from '@react-icons/all-files/fa/FaExternalLinkAlt';
|
||||
import { IoArrowBackCircleSharp } from '@react-icons/all-files/io5/IoArrowBackCircleSharp';
|
||||
|
|
@ -26,7 +27,7 @@ import {
|
|||
|
||||
export const IconLibrary = Object.freeze({
|
||||
back: IoArrowBackCircleSharp,
|
||||
betaTag: BetaTag,
|
||||
'beta-tag': BetaTag,
|
||||
branch: BiGitBranch,
|
||||
check: AiOutlineCheck,
|
||||
'check-circle': IoCheckmarkCircleSharp,
|
||||
|
|
@ -36,10 +37,11 @@ export const IconLibrary = Object.freeze({
|
|||
error: ErrorIcon,
|
||||
ethereum: EthereumIcon,
|
||||
'external-link': FaExternalLinkAlt,
|
||||
fleekLogo: FleekLogo,
|
||||
fleekName: FleekName,
|
||||
'fleek-logo': FleekLogo,
|
||||
'fleek-name': FleekName,
|
||||
github: IoLogoGithub,
|
||||
info: IoInformationCircleSharp,
|
||||
menu: FaBars,
|
||||
metamask: MetamaskIcon, //remove if not used
|
||||
search: BiSearch,
|
||||
square: BsFillSquareFill,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const ConnectWalletButton: React.FC = () => {
|
|||
if (ensName && address) setEnsNameStore(ensName, address);
|
||||
|
||||
return (
|
||||
<Button onClick={show}>
|
||||
<Button onClick={show} css={{ gridArea: 'wallet' }}>
|
||||
{isConnected && !!address && !!truncatedAddress ? (
|
||||
<Flex css={{ gap: '$2' }}>
|
||||
<Avatar address={address} size={20} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Icon } from '../../core/icon';
|
||||
import { NavBarStyles as S } from './nav-bar.styles';
|
||||
|
||||
export const Logo: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<S.Logo.Wrapper onClick={() => navigate('/home')}>
|
||||
<Icon
|
||||
name="fleek-logo"
|
||||
css={{ fontSize: '$2xl' }}
|
||||
iconElementCss={{ height: '$6' }}
|
||||
/>
|
||||
<Icon name="fleek-name" css={{ fontSize: '$6xl', mr: '$3' }} />
|
||||
<Icon name="beta-tag" css={{ fontSize: '$5xl' }} />
|
||||
</S.Logo.Wrapper>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,33 +1,129 @@
|
|||
import { styled } from '@/theme';
|
||||
import { StyledButton } from '@/components/core';
|
||||
import { alphaColor, styled } from '@/theme';
|
||||
|
||||
import { Flex } from '../flex.styles';
|
||||
|
||||
export abstract class NavBarStyles {
|
||||
static readonly Container = styled('header', {
|
||||
export const NavBarStyles = {
|
||||
Container: styled('header', {
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '$black',
|
||||
zIndex: '$sticky',
|
||||
height: '$22',
|
||||
overflow: 'hidden', // TODO: this must be worked on for responsive layout
|
||||
});
|
||||
|
||||
static readonly Content = styled('div', {
|
||||
display: 'flex',
|
||||
'&:after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: alphaColor('black', 0.8),
|
||||
backdropFilter: 'blur(4px)',
|
||||
zIndex: -1,
|
||||
},
|
||||
}),
|
||||
|
||||
Content: styled('div', {
|
||||
width: '100%',
|
||||
maxWidth: '$7xl',
|
||||
margin: '0 auto',
|
||||
alignItems: 'center',
|
||||
padding: '$6',
|
||||
});
|
||||
gap: '$3',
|
||||
|
||||
static readonly Navigation = styled(Flex, {
|
||||
gap: '$10',
|
||||
flexGrow: 4,
|
||||
justifyContent: 'center',
|
||||
});
|
||||
}
|
||||
display: 'grid',
|
||||
gridTemplateAreas: '"logo navigation wallet menu"',
|
||||
gridTemplateColumns: 'auto 1fr auto',
|
||||
}),
|
||||
|
||||
Navigation: {
|
||||
Container: styled(Flex, {
|
||||
gridArea: 'navigation',
|
||||
gap: '$10',
|
||||
justifyContent: 'center',
|
||||
|
||||
fontSize: '$lg',
|
||||
|
||||
variants: {
|
||||
stacked: {
|
||||
true: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '$4',
|
||||
|
||||
[`${StyledButton}`]: {
|
||||
fontSize: '$lg',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
Button: styled(StyledButton, {
|
||||
variants: {
|
||||
active: {
|
||||
true: {
|
||||
color: '$slate12 !important',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Sidebar: {
|
||||
Content: styled(Flex, {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: '$6',
|
||||
minWidth: '40vw',
|
||||
zIndex: '$sticky',
|
||||
backgroundColor: '$black',
|
||||
transition: 'transform 0.3s ease-in-out',
|
||||
borderLeft: '1px solid $slate6',
|
||||
|
||||
variants: {
|
||||
open: {
|
||||
true: {
|
||||
transform: 'translateX(0%)',
|
||||
},
|
||||
false: {
|
||||
transform: 'translateX(100%)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
Backdrop: styled('div', {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: '$sticky',
|
||||
backgroundColor: alphaColor('black', 0.5),
|
||||
display: 'none',
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
|
||||
variants: {
|
||||
open: {
|
||||
true: {
|
||||
display: 'block',
|
||||
backdropFilter: 'blur(4px)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Logo: {
|
||||
Wrapper: styled(Flex, {
|
||||
gridArea: 'logo',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,29 +1,20 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Button } from '@/components/core';
|
||||
import { Logo } from '@/components/logo/logo';
|
||||
import { useMediaQuery } from '@/hooks';
|
||||
|
||||
import { ConnectWalletButton } from './connect-wallet-button';
|
||||
import { Logo } from './logo';
|
||||
import { NavBarStyles as Styles } from './nav-bar.styles';
|
||||
import { Navigation } from './navigation';
|
||||
import { Sidebar } from './sidebar';
|
||||
|
||||
export const NavBar: React.FC = () => {
|
||||
const enableSidebar = useMediaQuery('(max-width: 540px)');
|
||||
|
||||
return (
|
||||
<Styles.Container>
|
||||
<Styles.Content>
|
||||
<Logo />
|
||||
|
||||
<Styles.Navigation>
|
||||
<Button as={Link} to="/explore" variant="link" color="gray">
|
||||
Explore
|
||||
</Button>
|
||||
<Button as={Link} to="/mint" variant="link" color="gray">
|
||||
Create
|
||||
</Button>
|
||||
<Button as={Link} to="/" variant="link" color="gray">
|
||||
Learn
|
||||
</Button>
|
||||
</Styles.Navigation>
|
||||
<ConnectWalletButton />
|
||||
{enableSidebar ? <Sidebar /> : <Navigation />}
|
||||
</Styles.Content>
|
||||
</Styles.Container>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import { forwardStyledRef } from '@/theme';
|
||||
|
||||
import { NavBarStyles as S } from './nav-bar.styles';
|
||||
|
||||
const Paths = [
|
||||
{ path: '/explore', name: 'Explore', activeRegex: /\/$|\/explore/ },
|
||||
{ path: '/mint', name: 'Create', activeRegex: /\/mint/ },
|
||||
{ path: '/', name: 'Learn', activeRegex: /\/learn/ },
|
||||
];
|
||||
|
||||
export const Navigation = forwardStyledRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithRef<typeof S.Navigation.Container>
|
||||
>((props, ref) => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<S.Navigation.Container {...props} ref={ref}>
|
||||
{Paths.map(({ path, name, activeRegex }) => (
|
||||
<S.Navigation.Button
|
||||
key={path}
|
||||
as={Link}
|
||||
to={path}
|
||||
active={activeRegex.test(location.pathname)}
|
||||
variant="link"
|
||||
color="gray"
|
||||
>
|
||||
{name}
|
||||
</S.Navigation.Button>
|
||||
))}
|
||||
</S.Navigation.Container>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Button, Icon } from '@/components/core';
|
||||
|
||||
import { NavBarStyles as Styles } from './nav-bar.styles';
|
||||
import { Navigation } from './navigation';
|
||||
|
||||
export const Sidebar: React.FC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleToggle = (): void => setIsOpen(!isOpen);
|
||||
|
||||
const handleNavigationClick = (): void => setIsOpen(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const { current } = sidebarRef;
|
||||
if (!current) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent): void => {
|
||||
if (current && !current.contains(event.target as Node)) setIsOpen(false);
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isOpen, sidebarRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleToggle}
|
||||
css={{ gridArea: 'menu', fontSize: '$lg' }}
|
||||
>
|
||||
<Icon name="menu" />
|
||||
</Button>
|
||||
|
||||
<Styles.Sidebar.Backdrop open={isOpen} />
|
||||
|
||||
<Styles.Sidebar.Content open={isOpen} ref={sidebarRef}>
|
||||
<Navigation stacked onClick={handleNavigationClick} />
|
||||
</Styles.Sidebar.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './logo';
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { styled } from '@/theme';
|
||||
|
||||
import { Flex } from '../layout';
|
||||
|
||||
export abstract class LogoStyles {
|
||||
static readonly Container = styled(Flex, {
|
||||
cursor: 'pointer',
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
static readonly Logo = styled('img', {
|
||||
width: '$6',
|
||||
height: 'auto',
|
||||
});
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Icon } from '../core/icon';
|
||||
import { LogoStyles as LS } from './logo.styles';
|
||||
|
||||
export const Logo: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<LS.Container onClick={() => navigate('/home')}>
|
||||
<Icon
|
||||
name="fleekLogo"
|
||||
css={{ fontSize: '$2xl' }}
|
||||
iconElementCss={{ height: '$6' }}
|
||||
/>
|
||||
<Icon name="fleekName" css={{ fontSize: '$6xl', mr: '$3' }} />
|
||||
<Icon name="betaTag" css={{ fontSize: '$5xl' }} />
|
||||
</LS.Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './use-transaction-cost';
|
||||
export * from './use-window-scroll-end';
|
||||
export * from './use-debounce';
|
||||
export * from './use-media-query';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useMediaQuery = (query: string): boolean => {
|
||||
const getMatches = (query: string): boolean => {
|
||||
// Prevents SSR issues
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.matchMedia(query).matches;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const [matches, setMatches] = useState<boolean>(getMatches(query));
|
||||
|
||||
const handleChange = (): void => {
|
||||
setMatches(getMatches(query));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const matchMedia = window.matchMedia(query);
|
||||
|
||||
// Triggered at the first client-side load and if query changes
|
||||
handleChange();
|
||||
|
||||
// Listen matchMedia
|
||||
if (matchMedia.addListener) {
|
||||
matchMedia.addListener(handleChange);
|
||||
} else {
|
||||
matchMedia.addEventListener('change', handleChange);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (matchMedia.removeListener) {
|
||||
matchMedia.removeListener(handleChange);
|
||||
} else {
|
||||
matchMedia.removeEventListener('change', handleChange);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query]);
|
||||
|
||||
return matches;
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { config, theme } from './themes';
|
||||
|
||||
config.theme.colors;
|
||||
export const alphaColor = (
|
||||
color: keyof typeof theme.colors,
|
||||
value: number
|
||||
): string =>
|
||||
config.theme.colors[color] +
|
||||
`00${Math.round(0xff * value).toString(16)}`.slice(-2);
|
||||
|
|
@ -23,6 +23,7 @@ export const colors = {
|
|||
...amber,
|
||||
};
|
||||
export const darkColors = {
|
||||
black: '#000000',
|
||||
...grayDark,
|
||||
...slateDark,
|
||||
...blueDark,
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ export * from './themes';
|
|||
export * from './foundations';
|
||||
export * from './key-frames';
|
||||
export * from './forward-styled-ref';
|
||||
export * from './alpha-color';
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ const createDripStitches = <
|
|||
},
|
||||
theme: {
|
||||
colors: {
|
||||
black: '#000000',
|
||||
...darkColors, // TODO: replace with light colors once it's done the light mode
|
||||
...(theme?.colors || {}),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue