2
0

refactor(lp): ♻️ Simplify header

This commit is contained in:
Baptiste Arnaud
2022-04-05 09:51:43 +02:00
parent ceedb05b64
commit 1fdf7e734b
29 changed files with 512 additions and 790 deletions

View File

@ -0,0 +1,109 @@
import {
Button,
Flex,
Heading,
HStack,
IconButton,
useColorModeValue as mode,
useDisclosure,
Box,
} from '@chakra-ui/react'
import { HamburgerIcon } from 'assets/icons'
import { ChevronDownIcon } from 'assets/icons/ChevronDownIcon'
import { CloseIcon } from 'assets/icons/CloseIcon'
import { Logo } from 'assets/icons/Logo'
import * as React from 'react'
import { NextChakraLink } from '../nextChakraAdapters/NextChakraLink'
import { MobileMenu } from './MobileMenu'
import { ResourcesMenu } from './ResourcesMenu'
export const Header = () => {
const { isOpen, onToggle } = useDisclosure()
const { isOpen: isMobileMenuOpen, onToggle: onMobileMenuToggle } =
useDisclosure()
return (
<Flex pos="relative" zIndex={10} w="full">
<HStack
as="header"
aria-label="Main navigation"
maxW="7xl"
w="full"
mx="auto"
px={{ base: '6', md: '8' }}
py="4"
justify="space-between"
>
<Flex
align="center"
justify="space-between"
className="nav-content__mobile"
color={mode('white', 'white')}
>
<HStack as={NextChakraLink} href="/" rel="home" ml="2">
<Logo boxSize="35px" />
<Heading as="p" fontSize="lg">
Typebot
</Heading>
</HStack>
</Flex>
<Box display={['block', 'block', 'none']}>
<IconButton
aria-label={'Open menu'}
icon={
isMobileMenuOpen ? (
<CloseIcon boxSize="20px" />
) : (
<HamburgerIcon boxSize="20px" />
)
}
variant="ghost"
colorScheme="gray"
onClick={onMobileMenuToggle}
/>
<MobileMenu isOpen={isMobileMenuOpen} />
</Box>
<HStack as="nav" spacing={4} display={['none', 'none', 'flex']}>
<Flex>
<Button
rightIcon={<ChevronDownIcon />}
onClick={onToggle}
variant="ghost"
colorScheme="gray"
fontWeight={700}
>
Resources
</Button>
<ResourcesMenu isOpen={isOpen} />
</Flex>
<Button
as={NextChakraLink}
href="/pricing"
variant="ghost"
colorScheme="gray"
fontWeight={700}
>
Pricing
</Button>
<Button
as={NextChakraLink}
href="https://app.typebot.io/signin"
colorScheme="blue"
variant="outline"
fontWeight={700}
>
Sign in
</Button>
<Button
as={NextChakraLink}
href="https://app.typebot.io/register"
colorScheme="orange"
fontWeight={700}
>
Create a typebot
</Button>
</HStack>
</HStack>
</Flex>
)
}

View File

@ -0,0 +1,59 @@
import { Collapse, Stack, Button, Text } from '@chakra-ui/react'
import { NextChakraLink } from '../nextChakraAdapters/NextChakraLink'
import { links } from './_data'
type Props = { isOpen: boolean }
export const MobileMenu = ({ isOpen }: Props) => (
<Collapse in={isOpen}>
<Stack
pos="absolute"
insetX={0}
bgGradient="linear(to-b, gray.900, gray.800)"
px="6"
py="10"
spacing={4}
>
<Button
as={NextChakraLink}
href="https://app.typebot.io/signin"
colorScheme="blue"
variant="outline"
fontWeight={700}
>
Sign in
</Button>
<Button
as={NextChakraLink}
href="https://app.typebot.io/register"
colorScheme="orange"
fontWeight={700}
>
Create a typebot
</Button>
<Button
as={NextChakraLink}
href="/pricing"
variant="outline"
colorScheme="gray"
fontWeight={700}
>
Pricing
</Button>
<Text fontWeight="700">Resources:</Text>
{links[0].children?.map((link, idx) => (
<Button
as={NextChakraLink}
href={link.href}
key={idx}
variant="outline"
colorScheme="gray"
fontWeight={700}
py="6"
>
{link.label}
</Button>
))}
</Stack>
</Collapse>
)

View File

@ -0,0 +1,89 @@
import {
Box,
Center,
Collapse,
HStack,
SimpleGrid,
Text,
useColorModeValue as mode,
} from '@chakra-ui/react'
import { ChevronRightIcon } from 'assets/icons/ChevronRightIcon'
import * as React from 'react'
import { NextChakraLink } from '../nextChakraAdapters/NextChakraLink'
import { links } from './_data'
type Props = { isOpen: boolean }
export const ResourcesMenu = ({ isOpen }: Props) => (
<Collapse in={isOpen} animateOpacity unmountOnExit={false}>
<Box
w="full"
shadow="lg"
pos="absolute"
insetX={0}
top="16"
py="12"
px="4"
bgGradient="linear(to-b, gray.900, gray.800)"
>
<Box maxW="7xl" mx="auto" px="8">
<SimpleGrid spacing="10" columns={2}>
{links[0].children?.map((item, idx) => (
<NextChakraLink
key={idx}
className="group"
href={item.href}
m="-3"
p="3"
display="flex"
alignItems="flex-start"
transition="all 0.2s"
rounded="lg"
_hover={{ bg: mode('gray.50', 'gray.600') }}
_focus={{ shadow: 'outline' }}
isExternal={
item.href.startsWith('https') &&
!item.href.includes('app.typebot.io')
}
>
<Center
aria-hidden
as="span"
flexShrink={0}
w="10"
h="10"
fontSize="3xl"
color={'blue.300'}
>
{item.icon}
</Center>
<Box marginStart="3" as="dl">
<HStack as="dt">
<Text
fontWeight="semibold"
color={mode('gray.900', 'white')}
_groupHover={{ color: mode('blue.600', 'inherit') }}
>
{item.label}
</Text>
<Box
fontSize="xs"
as={ChevronRightIcon}
transition="all 0.2s"
_groupHover={{
color: mode('blue.600', 'inherit'),
transform: 'translateX(2px)',
}}
/>
</HStack>
<Text as="dd" color={mode('gray.500', 'gray.400')}>
{item.description}
</Text>
</Box>
</NextChakraLink>
))}
</SimpleGrid>
</Box>
</Box>
</Collapse>
)

View File

@ -4,17 +4,6 @@ import { MapIcon } from 'assets/icons/MapIcon'
import { PeopleCircleIcon } from 'assets/icons/PeopleCircleIcon'
import * as React from 'react'
export interface Link {
label: string
href?: string
children?: Array<{
label: string
description?: string
href: string
icon?: React.ReactElement
}>
}
export const links = [
{
label: 'Resources',

View File

@ -1,5 +0,0 @@
import { Box, BoxProps } from '@chakra-ui/react'
import { HTMLMotionProps, motion } from 'framer-motion'
export type MotionBoxProps = BoxProps & HTMLMotionProps<'div'>
export const MotionBox = motion<BoxProps>(Box)

View File

@ -1,143 +0,0 @@
import {
Box,
Button,
Flex,
FlexProps,
HStack,
useDisclosure,
useColorModeValue as mode,
Heading,
} from '@chakra-ui/react'
import * as React from 'react'
import { Logo } from 'assets/icons/Logo'
import { NextChakraLink } from '../nextChakraAdapters/NextChakraLink'
import { NavLink } from './NavLink'
import { NavMenu } from './NavMenu'
import { Submenu } from './Submenu'
import { ToggleButton } from './ToggleButton'
import { Link } from './_data'
const MobileNavContext = ({
links,
...props
}: { links: Link[] } & FlexProps) => {
const { isOpen, onToggle } = useDisclosure()
return (
<>
<Flex
align="center"
justify="space-between"
className="nav-content__mobile"
color={mode('white', 'white')}
{...props}
>
<HStack as={NextChakraLink} href="/" rel="home" ml="2">
<Logo boxSize="35px" />
<Heading as="p" fontSize="lg">
Typebot
</Heading>
</HStack>
<Box>
<ToggleButton isOpen={isOpen} onClick={onToggle} />
</Box>
</Flex>
<NavMenu animate={isOpen ? 'open' : 'closed'}>
{links.map((link, idx) =>
link.children ? (
<Submenu.Mobile key={idx} link={link} />
) : (
<NavLink.Mobile key={idx} href={link.href ?? '#'}>
{link.label}
</NavLink.Mobile>
)
)}
<Button
as={NextChakraLink}
href="https://app.typebot.io/signin"
colorScheme="blue"
variant="outline"
w="full"
size="lg"
mt="5"
>
Sign in
</Button>
<Button
as={NextChakraLink}
href="https://app.typebot.io/register"
colorScheme="orange"
w="full"
size="lg"
mt="5"
>
Create a typebot for free
</Button>
</NavMenu>
</>
)
}
const DesktopNavContent = ({
links,
...props
}: { links: Link[] } & FlexProps) => {
return (
<Flex
className="nav-content__desktop"
align="center"
justify="space-between"
{...props}
color={mode('bg.gray800', 'white')}
>
<HStack as={NextChakraLink} href="/" rel="home">
<Logo boxSize="35px" />
<Heading as="p" fontSize="lg">
Typebot
</Heading>
</HStack>
<HStack spacing="4" minW="240px" justify="space-between">
<HStack
as="ul"
id="nav__primary-menu"
aria-label="Main Menu"
listStyleType="none"
>
{links.map((link, idx) => (
<Box as="li" key={idx} id={`nav__menuitem-${idx}`}>
{link.children ? (
<Submenu.Desktop link={link} />
) : (
<NavLink.Desktop href={link.href ?? '#'}>
{link.label}
</NavLink.Desktop>
)}
</Box>
))}
</HStack>
<Button
as={NextChakraLink}
href="https://app.typebot.io/signin"
colorScheme="blue"
variant="outline"
fontWeight="bold"
>
Sign in
</Button>
<Button
as={NextChakraLink}
href="https://app.typebot.io/register"
colorScheme="orange"
fontWeight="bold"
>
Create a typebot
</Button>
</HStack>
</Flex>
)
}
export const NavContent = {
Mobile: MobileNavContext,
Desktop: DesktopNavContent,
}

View File

@ -1,48 +0,0 @@
import { LinkProps as ChakraLinkProps, Button } from '@chakra-ui/react'
import { LinkProps as NextLinkProps } from 'next/link'
import * as React from 'react'
import { NextChakraLink } from '../nextChakraAdapters/NextChakraLink'
type NavLinkProps = NextLinkProps &
Omit<ChakraLinkProps, 'as'> & {
active?: boolean
}
const DesktopNavLink = (props: NavLinkProps) => {
const { href, children } = props
return (
<Button
as={NextChakraLink}
href={href}
isExternal={href.startsWith('https') && !href.includes('app.typebot.io')}
variant="ghost"
colorScheme="gray"
>
{children}
</Button>
)
}
DesktopNavLink.displayName = 'DesktopNavLink'
export const MobileNavLink = (props: NavLinkProps) => {
const { href, children } = props
return (
<Button
as={NextChakraLink}
href={href ?? '#'}
isExternal={href.startsWith('https') && !href.includes('app.typebot.io')}
variant="ghost"
colorScheme="gray"
w="full"
h="3rem"
justifyContent="flex-start"
>
{children}
</Button>
)
}
export const NavLink = {
Mobile: MobileNavLink,
Desktop: DesktopNavLink,
}

View File

@ -1,43 +0,0 @@
import { Variants } from 'framer-motion'
import * as React from 'react'
import { MotionBox, MotionBoxProps } from './MotionBox'
export const NavMenu = (props: MotionBoxProps) => (
<MotionBox
initial="init"
variants={variants}
outline="0"
opacity="0"
bgGradient="linear(to-b, gray.900, gray.800)"
w="full"
shadow="lg"
px="4"
pos="absolute"
insetX="0"
py="12"
{...props}
/>
)
const variants: Variants = {
init: {
opacity: 0,
y: -4,
display: 'none',
transition: { duration: 0 },
},
open: {
opacity: 1,
y: 0,
display: 'block',
transition: { duration: 0.15 },
},
closed: {
opacity: 0,
y: -4,
transition: { duration: 0.1 },
transitionEnd: {
display: 'none',
},
},
}

View File

@ -1,30 +0,0 @@
import { Box } from '@chakra-ui/react'
import * as React from 'react'
import { NavContent } from './NavContent'
import { links } from './_data'
export const Navbar = () => {
return (
<Box w="full">
<Box as="header" position="relative" zIndex="10">
<Box
as="nav"
aria-label="Main navigation"
maxW="7xl"
mx="auto"
px={{ base: '6', md: '8' }}
py="4"
>
<NavContent.Mobile
display={{ base: 'flex', lg: 'none' }}
links={links}
/>
<NavContent.Desktop
display={{ base: 'none', lg: 'flex' }}
links={links}
/>
</Box>
</Box>
</Box>
)
}

View File

@ -1,95 +0,0 @@
import { useNavMenu } from './useNavMenu'
import {
Box,
Collapse,
SimpleGrid,
useDisclosure,
Button,
} from '@chakra-ui/react'
import * as React from 'react'
import { Link } from './_data'
import { NavLink } from './NavLink'
import { NavMenu } from './NavMenu'
import { SubmenuItem as DesktopMenuItem } from './SubmenuItem'
import { ChevronDownIcon } from '../../../assets/icons/ChevronDownIcon'
interface SubmenuProps {
link: Link
}
const DesktopSubmenu = (props: SubmenuProps) => {
const { link } = props
const { isOpen, getMenuProps, getTriggerProps } = useNavMenu()
return (
<>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Button
rightIcon={<ChevronDownIcon />}
variant="ghost"
colorScheme="gray"
{...getTriggerProps()}
>
{link.label}
</Button>
<NavMenu {...getMenuProps()} animate={isOpen ? 'open' : 'closed'}>
<Box maxW="7xl" mx="auto" px="8">
<SimpleGrid spacing="10" columns={2}>
{link.children?.map((item, idx) => (
<DesktopMenuItem
key={idx}
title={item.label}
href={item.href ?? '#'}
icon={item.icon}
>
{item.description}
</DesktopMenuItem>
))}
</SimpleGrid>
</Box>
</NavMenu>
</>
)
}
const MobileSubMenu = (props: SubmenuProps) => {
const { link } = props
const { isOpen, onToggle } = useDisclosure()
return (
<Box>
<Button
textAlign="start"
type="button"
cursor="pointer"
onClick={onToggle}
paddingEnd="4"
variant="ghost"
colorScheme="gray"
h="3rem"
w="full"
justifyContent="flex-start"
>
<Box mr="4">{link.label}</Box>
<Box
as={ChevronDownIcon}
transform={`rotate(${isOpen ? '180deg' : '0deg'})`}
/>
</Button>
<Collapse in={isOpen}>
<Box pl="5">
{link.children?.map((item, idx) => (
<NavLink.Mobile key={idx} href={item.href ?? '#'}>
{item.label}
</NavLink.Mobile>
))}
</Box>
</Collapse>
</Box>
)
}
export const Submenu = {
Mobile: MobileSubMenu,
Desktop: DesktopSubmenu,
}

View File

@ -1,75 +0,0 @@
import {
Box,
Center,
HStack,
Text,
useColorModeValue as mode,
LinkProps as ChakraLinkProps,
} from '@chakra-ui/react'
import { LinkProps } from 'next/link'
import * as React from 'react'
import { ChevronRightIcon } from '../../../assets/icons/ChevronRightIcon'
import { NextChakraLink } from '../nextChakraAdapters/NextChakraLink'
type SubmenuItemProps = LinkProps &
Omit<ChakraLinkProps, 'as'> & {
title: string
icon?: React.ReactElement
children: React.ReactNode
href: string
}
export const SubmenuItem = (props: SubmenuItemProps) => {
const { title, icon, children, href, ...rest } = props
return (
<NextChakraLink
className="group"
href={href}
m="-3"
p="3"
display="flex"
alignItems="flex-start"
transition="all 0.2s"
rounded="lg"
_hover={{ bg: mode('gray.50', 'gray.600') }}
_focus={{ shadow: 'outline' }}
isExternal={href.startsWith('https') && !href.includes('app.typebot.io')}
{...rest}
>
<Center
aria-hidden
as="span"
flexShrink={0}
w="10"
h="10"
fontSize="3xl"
color={'blue.300'}
>
{icon}
</Center>
<Box marginStart="3" as="dl">
<HStack as="dt">
<Text
fontWeight="semibold"
color={mode('gray.900', 'white')}
_groupHover={{ color: mode('blue.600', 'inherit') }}
>
{title}
</Text>
<Box
fontSize="xs"
as={ChevronRightIcon}
transition="all 0.2s"
_groupHover={{
color: mode('blue.600', 'inherit'),
transform: 'translateX(2px)',
}}
/>
</HStack>
<Text as="dd" color={mode('gray.500', 'gray.400')}>
{children}
</Text>
</Box>
</NextChakraLink>
)
}

View File

@ -1,65 +0,0 @@
import { Box, Center, chakra, VisuallyHidden } from '@chakra-ui/react'
import React from 'react'
const Bar = chakra('span', {
baseStyle: {
display: 'block',
pos: 'absolute',
w: '1.25rem',
h: '0.125rem',
rounded: 'full',
bg: 'currentcolor',
mx: 'auto',
insetStart: '0.125rem',
transition: 'all 0.12s',
},
})
const ToggleIcon = (props: { active: boolean }) => {
const { active } = props
return (
<Box
className="group"
data-active={active ? '' : undefined}
as="span"
display="block"
w="1.5rem"
h="1.5rem"
pos="relative"
aria-hidden
pointerEvents="none"
>
<Bar
top="0.4375rem"
_groupActive={{ top: '0.6875rem', transform: 'rotate(45deg)' }}
/>
<Bar
bottom="0.4375rem"
_groupActive={{ bottom: '0.6875rem', transform: 'rotate(-45deg)' }}
/>
</Box>
)
}
interface ToggleButtonProps {
isOpen: boolean
onClick(): void
}
export const ToggleButton = (props: ToggleButtonProps) => {
const { isOpen, onClick } = props
return (
<Center
marginStart="-6"
px="4"
py="4"
as="button"
color="gray.400"
_active={{ color: 'blue.600' }}
onClick={onClick}
>
<ToggleIcon active={isOpen} />
<VisuallyHidden>Toggle Menu</VisuallyHidden>
</Center>
)
}

View File

@ -1,127 +0,0 @@
import { useDisclosure } from '@chakra-ui/react'
import { isFocusable, getOwnerDocument, isRightClick } from '@chakra-ui/utils'
import * as React from 'react'
const getTarget = (event: React.FocusEvent) =>
(event.relatedTarget || document.activeElement) as HTMLElement
type OmitMotionProps<T> = Omit<
T,
'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag'
>
export function useNavMenu() {
const { isOpen, onClose, onToggle, onOpen } = useDisclosure()
const menuRef = React.useRef<HTMLDivElement>(null)
const triggerRef = React.useRef<HTMLAnchorElement>(null)
const timeoutRef = React.useRef<number>()
React.useEffect(() => {
return () => {
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current)
}
}
}, [])
const focusMenu = () => {
timeoutRef.current = window.setTimeout(() => {
menuRef.current?.focus({ preventScroll: true })
}, 100)
}
const getTriggerProps = () => {
const triggerProps: React.ComponentPropsWithRef<'a'> = {
ref: triggerRef,
'aria-expanded': isOpen,
'aria-controls': 'menu',
'aria-haspopup': 'true',
}
triggerProps.onClick = (event: React.MouseEvent) => {
if (isRightClick(event)) return
onToggle()
if (!isOpen) {
focusMenu()
}
}
triggerProps.onBlur = (event: React.FocusEvent) => {
const target = getTarget(event)
if (isOpen && !menuRef.current?.contains(target)) {
onClose()
}
}
triggerProps.onKeyDown = (event: React.KeyboardEvent) => {
if (isOpen && event.key === 'Escape') {
onClose()
triggerRef.current?.focus({ preventScroll: true })
}
if (event.key === 'ArrowDown') {
if (!isOpen) onOpen()
focusMenu()
}
}
return triggerProps
}
const getMenuProps = () => {
const menuProps: OmitMotionProps<React.ComponentPropsWithRef<'div'>> = {
ref: menuRef,
id: 'menu',
tabIndex: -1,
}
menuProps.onKeyDown = (event: React.KeyboardEvent) => {
if (!isOpen) return
switch (event.key) {
case 'Escape': {
onClose()
return triggerRef.current?.focus()
}
case 'ArrowDown': {
const doc = getOwnerDocument(menuRef.current)
const next = doc?.activeElement
?.nextElementSibling as HTMLAnchorElement | null
return next?.focus()
}
case 'ArrowUp': {
const doc = getOwnerDocument(menuRef.current)
const prev = doc?.activeElement
?.previousElementSibling as HTMLAnchorElement | null
const el = (prev ?? triggerRef.current) as HTMLElement
return el.focus()
}
default:
break
}
}
menuProps.onBlur = (event: React.FocusEvent) => {
const target = getTarget(event)
const shouldBlur =
isOpen &&
!target.isSameNode(triggerRef.current) &&
!menuRef.current?.contains(target)
if (shouldBlur) {
onClose()
if (!isFocusable(target)) {
triggerRef.current?.focus({ preventScroll: true })
}
}
}
return menuProps
}
return {
isOpen,
onClose,
getTriggerProps,
getMenuProps,
}
}