Add navbar

This commit is contained in:
Lachlan Campbell 2020-04-24 10:21:46 -04:00
parent c597e58218
commit 294dc519d6
5 changed files with 343 additions and 15 deletions

View file

@ -1,19 +1,65 @@
import { Link as A, Image } from 'theme-ui'
import theme from '../lib/theme'
import styled from '@emotion/styled'
import { css, keyframes } from '@emotion/core'
import Link from 'next/link'
const Flag = () => (
<A
href="https://hackclub.com/"
target="_blank"
rel="noopener noreferrer"
aria-label="Hack Club homepage"
sx={{ ineHeight: 0, position: 'absolute', top: 0, left: 3 }}
>
<Image
src="https://assets.hackclub.com/flag-orpheus-top.svg"
alt="Hack Club flag"
sx={{ width: [96, 128] }}
/>
</A>
const waveFlag = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(-5deg);
}
`
const waveFlagScaled = keyframes`
from {
transform: scale(.75) rotate(0deg);
}
to {
transform: scale(.75) rotate(-5deg);
}
`
const scrolled = (props) =>
props.scrolled &&
css`
transform: scale(0.875);
height: 56px;
&:hover,
&:focus {
animation: ${waveFlagScaled} 0.5s linear infinite alternate;
}
`
const Base = styled('a')`
background: url(https://assets.hackclub.com/flag-orpheus-top.svg) no-repeat;
background-position: top center;
background-size: contain;
cursor: pointer;
flex-shrink: 0;
width: 112px;
height: 48px;
transition: ${3 / 16}s cubic-bezier(0.375, 0, 0.675, 1) transform;
transform-origin: top left;
@media (min-width: ${theme.breakpoints[1]}) {
width: 172px;
height: 64px;
}
&:hover,
&:focus {
animation: ${waveFlag} 0.5s linear infinite alternate;
}
@media (prefers-reduced-motion: reduce) {
animation: none !important;
}
${scrolled};
`
const Flag = (props) => (
<Link href="/" aria-label="Homepage" passHref>
<Base {...props} />
</Link>
)
export default Flag

267
components/nav.js Normal file
View file

@ -0,0 +1,267 @@
import React, { Component } from 'react'
import styled from '@emotion/styled'
import { css, keyframes } from '@emotion/core'
import { Box, Container, Flex, Link } from 'theme-ui'
import theme from '../lib/theme'
import Icon from './icon'
import Flag from './flag'
import ScrollLock from 'react-scrolllock'
const rgbaBgColor = (props, opacity) =>
`rgba(
${props.bgColor[0]},
${props.bgColor[1]},
${props.bgColor[2]},
${opacity}
)`
const unfixed = (props) =>
!props.unfixed &&
css`
position: absolute;
top: 0;
`
// const bg = (props) =>
// props.dark
// ? css`
// -webkit-backdrop-filter: saturate(90%) blur(20px);
// backdrop-filter: saturate(90%) blur(20px);
// `
// : css`
// -webkit-backdrop-filter: saturate(180%) blur(20px);
// backdrop-filter: saturate(180%) blur(20px);
// `
const fixed = (props) =>
(props.scrolled || props.toggled || props.fixed) &&
css`
position: fixed;
background-color: ${rgbaBgColor(props, 0.96875)};
border-bottom: 1px solid rgba(48, 48, 48, 0.125);
@supports (-webkit-backdrop-filter: none) or (backdrop-filter: none) {
background-color: ${props.transparent
? 'transparent'
: rgbaBgColor(props, 0.75)};
-webkit-backdrop-filter: saturate(180%) blur(20px);
backdrop-filter: saturate(180%) blur(20px);
/* {bg}; to support dark mode later */
}
`
const Root = styled(Box)`
${unfixed};
width: 100%;
z-index: 1000;
${fixed};
@media print {
display: none;
}
`
export const Content = styled(Container)`
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 2;
padding-left: ${theme.space[3]}px;
@media (min-width: ${theme.breakpoints[2]}em) {
padding: 0 ${theme.space[4]}px;
}
`
const hoverColor = (name) =>
({
white: 'smoke',
smoke: 'muted',
muted: 'slate',
slate: 'black',
black: 'slate',
primary: 'error'
}[name] || 'black')
const slide = keyframes({
from: { transform: 'translateY(-100%)' },
to: { transform: 'translateY(0)' }
})
const layout = (props) =>
props.isMobile
? css`
display: ${props.toggled ? 'flex' : 'none'};
flex-direction: column;
overflow-y: auto;
text-align: left;
height: 100vh;
animation: ${slide} 0.375s ease-in;
@media (prefers-reduced-motion: reduce) {
animation: none;
}
a {
color: ${theme.colors[props.dark ? 'white' : 'black']} !important;
margin: 0 auto;
height: 64px;
font-weight: bold;
font-size: ${theme.fontSizes[2]}px;
width: 100%;
max-width: 18rem;
&:not(:last-child) {
border-bottom: 1px solid rgba(48, 48, 48, 0.125);
}
@media screen and (max-width: 22em) {
max-width: 16rem;
}
}
`
: css`
@media (min-width: 56em) {
display: flex;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
a {
font-size: ${theme.fontSizes[1]}px;
text-transform: uppercase;
&:hover {
color: ${theme.colors[hoverColor(props.color)]};
}
}
`
const NavBar = styled(Box)`
display: none;
${layout};
a {
margin-left: ${theme.space[3]}px;
padding: ${theme.space[3]}px;
text-decoration: none;
@media (min-width: 56em) {
color: ${(props) => theme.colors[props.color] || color};
}
}
`
const Navigation = (props) => (
<NavBar role="navigation" {...props}>
<Link href="https://hackclub.com/clubs/" children="Clubs" />
<Link href="https://workshops.hackclub.com/" children="Workshops" />
<Link href="https://hackathons.hackclub.com/" children="Hackathons" />
<Link href="https://hackclub.com/bank/" children="Bank" />
<Link href="https://hackclub.com/donate/" children="Donate" />
</NavBar>
)
const ToggleContainer = styled(Flex)`
align-items: center;
justify-content: center;
min-width: 64px;
min-height: 44px;
cursor: pointer;
user-select: none;
margin-left: auto;
@media (min-width: 56em) {
display: none;
}
`
class Header extends Component {
state = {
scrolled: false,
toggled: false
}
static defaultProps = {
dark: false,
color: 'white'
}
componentDidMount() {
this.bindScroll(true)
if (typeof window !== 'undefined') {
const mobileQuery = window.matchMedia('(max-width: 48em)')
mobileQuery.addListener(() => {
this.setState({ mobile: true, toggled: false })
})
}
}
componentWillUnmount = () => {
this.bindScroll(false)
}
bindScroll = (add) => {
if (typeof window !== 'undefined' && !this.props.unfixed) {
window[add ? 'addEventListener' : 'removeEventListener'](
'scroll',
this.onScroll
)
}
}
onScroll = () => {
const newState = window.scrollY >= 16
const { scrolled: oldState } = this.state
if (newState !== oldState) {
this.setState({
scrolled: newState
})
}
}
handleToggleMenu = () => {
this.setState((state) => ({
toggled: !state.toggled
}))
}
render() {
const { color, dark, fixed, bgColor, ...props } = this.props
const { mobile, scrolled, toggled } = this.state
const baseColor = dark
? color || 'white'
: color === 'white' && scrolled
? 'black'
: color
const toggleColor = dark
? color || 'snow'
: toggled || (color === 'white' && scrolled)
? 'slate'
: color
return (
<Root
{...props}
fixed={fixed}
scrolled={scrolled}
toggled={toggled}
dark={dark}
bgColor={bgColor || (dark ? [32, 34, 36] : [255, 255, 255])}
as="header"
>
<Content>
<Flag scrolled={scrolled || fixed} />
<Navigation
as="nav"
aria-hidden={!!mobile}
color={baseColor}
dark={dark}
/>
<ToggleContainer color={toggleColor} onClick={this.handleToggleMenu}>
<Icon glyph={toggled ? 'view-close' : 'menu'} toggled={toggled} />
</ToggleContainer>
</Content>
<Navigation
as="nav"
aria-hidden={!mobile}
isMobile
toggled={toggled}
color={baseColor}
dark={dark}
/>
{toggled && <ScrollLock />}
</Root>
)
}
}
export default Header

View file

@ -21,6 +21,7 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-reveal": "^1.2.2",
"react-scrolllock": "^5.0.1",
"react-use-websocket": "^1.7.3",
"theme-ui": "^0.3.1"
}

View file

@ -10,6 +10,7 @@ import {
} from 'theme-ui'
import { keyframes } from '@emotion/core'
import { Slide } from 'react-reveal'
import Nav from '../components/nav'
import ForceTheme from '../components/force-theme'
import Footer from '../components/footer'
import SlackEvents from '../components/home/slack-events'
@ -107,6 +108,7 @@ const grad = (theme, from, to) => `radial-gradient(
export default () => (
<>
<ForceTheme theme="light" />
<Nav />
<Box
as="header"
sx={{

View file

@ -2899,6 +2899,11 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
md5.js "^1.3.4"
safe-buffer "^5.1.1"
exenv@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
expand-brackets@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
@ -5105,6 +5110,13 @@ react-reveal@^1.2.2:
dependencies:
prop-types "^15.5.10"
react-scrolllock@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-scrolllock/-/react-scrolllock-5.0.1.tgz#da1cfb7b6d55c86ae41dbad5274b778c307752b7"
integrity sha512-poeEsjnZAlpA6fJlaNo4rZtcip2j6l5mUGU/SJe1FFlicEudS943++u7ZSdA7lk10hoyYK3grOD02/qqt5Lxhw==
dependencies:
exenv "^1.2.2"
react-use-websocket@^1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/react-use-websocket/-/react-use-websocket-1.7.3.tgz#1f67d3b5140761f14b1a65ee2329071e9082597a"