mirror of
https://github.com/System-End/site.git
synced 2026-04-19 23:22:49 +00:00
Add navbar
This commit is contained in:
parent
c597e58218
commit
294dc519d6
5 changed files with 343 additions and 15 deletions
|
|
@ -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
267
components/nav.js
Normal 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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
12
yarn.lock
12
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue