chore(lp): 📦️ Import existing Landing page
This commit is contained in:
21
apps/landing-page/components/common/ArticleCta.tsx
Normal file
21
apps/landing-page/components/common/ArticleCta.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { VStack, Heading, Button, Text } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
|
||||
export const ArticleCallToAction = () => (
|
||||
<VStack spacing={6}>
|
||||
<Heading fontSize="xx-large">
|
||||
Collect up to 4x more responses without 4x the work.
|
||||
</Heading>
|
||||
<Button
|
||||
size="lg"
|
||||
colorScheme="orange"
|
||||
as="a"
|
||||
href="https://app.typebot.io/signup"
|
||||
>
|
||||
Create a typebot
|
||||
</Button>
|
||||
<Text fontSize="sm" fontStyle="italic" color="gray.600">
|
||||
It's free!
|
||||
</Text>
|
||||
</VStack>
|
||||
)
|
73
apps/landing-page/components/common/Footer.tsx
Normal file
73
apps/landing-page/components/common/Footer.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React, { ReactNode } from 'react'
|
||||
|
||||
import { Box, Container, Heading, SimpleGrid, Stack } from '@chakra-ui/react'
|
||||
import { NextChakraLink } from './nextChakraAdapters/NextChakraLink'
|
||||
import { Logo } from 'assets/icons/Logo'
|
||||
|
||||
const facebookGroupUrl = 'https://www.facebook.com/groups/typebot'
|
||||
const typebotLinkedInUrl = 'https://www.linkedin.com/company/typebot'
|
||||
const typebotTwitterUrl = 'https://twitter.com/Typebot_io'
|
||||
export const contactUrl = 'https://bot.typebot.io/landing-page-bubble-en'
|
||||
export const roadmapLink = 'https://feedback.typebot.io/roadmap'
|
||||
|
||||
export const Footer = () => {
|
||||
return (
|
||||
<Box w="full" bgColor="gray.50">
|
||||
<Container as={Stack} maxW={'1000px'} py={10}>
|
||||
<SimpleGrid columns={[1, 2, 5]} spacing={8} px={2}>
|
||||
<Stack spacing={6}>
|
||||
<Box>
|
||||
<Logo boxSize="30px" />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack align={'flex-start'}>
|
||||
<ListHeader>Product</ListHeader>
|
||||
<NextChakraLink href={roadmapLink} isExternal>
|
||||
Roadmap
|
||||
</NextChakraLink>
|
||||
<NextChakraLink href={'/blog'}>Blog</NextChakraLink>
|
||||
<NextChakraLink href={'/pricing'}>Pricing</NextChakraLink>
|
||||
</Stack>
|
||||
<Stack align={'flex-start'}>
|
||||
<ListHeader>Comparisons</ListHeader>
|
||||
<NextChakraLink href="/vs-typeform">VS Typeform</NextChakraLink>
|
||||
<NextChakraLink href="/vs-landbot">VS Landbot</NextChakraLink>
|
||||
<NextChakraLink href="/vs-tally">VS Tally</NextChakraLink>
|
||||
</Stack>
|
||||
<Stack align={'flex-start'}>
|
||||
<ListHeader>Community</ListHeader>
|
||||
<NextChakraLink href={facebookGroupUrl} isExternal>
|
||||
Facebook Group
|
||||
</NextChakraLink>
|
||||
<NextChakraLink href={typebotTwitterUrl} isExternal>
|
||||
Twitter
|
||||
</NextChakraLink>
|
||||
<NextChakraLink href={typebotLinkedInUrl} isExternal>
|
||||
LinkedIn
|
||||
</NextChakraLink>
|
||||
</Stack>
|
||||
<Stack align={'flex-start'}>
|
||||
<ListHeader>Company</ListHeader>
|
||||
<NextChakraLink href="mailto:baptiste@typebot.io">
|
||||
Contact
|
||||
</NextChakraLink>
|
||||
<NextChakraLink href={'/terms-of-service'}>
|
||||
Terms of Service
|
||||
</NextChakraLink>
|
||||
<NextChakraLink href={'/privacy-policies'}>
|
||||
Privacy Policy
|
||||
</NextChakraLink>
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const ListHeader = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Heading fontWeight={'500'} fontSize={'lg'} mb={2}>
|
||||
{children}
|
||||
</Heading>
|
||||
)
|
||||
}
|
5
apps/landing-page/components/common/Navbar/MotionBox.tsx
Executable file
5
apps/landing-page/components/common/Navbar/MotionBox.tsx
Executable file
@ -0,0 +1,5 @@
|
||||
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)
|
142
apps/landing-page/components/common/Navbar/NavContent.tsx
Executable file
142
apps/landing-page/components/common/Navbar/NavContent.tsx
Executable file
@ -0,0 +1,142 @@
|
||||
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/signup"
|
||||
colorScheme="blue"
|
||||
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" isDark={mode(false, true)} />
|
||||
<Heading as="p" fontSize="lg">
|
||||
Typebot
|
||||
</Heading>
|
||||
</HStack>
|
||||
<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>
|
||||
<HStack spacing="8" minW="240px" justify="space-between">
|
||||
<Box
|
||||
as={NextChakraLink}
|
||||
href="https://app.typebot.io/signin"
|
||||
colorScheme="blue"
|
||||
variant="ghost"
|
||||
fontWeight="bold"
|
||||
>
|
||||
Sign in
|
||||
</Box>
|
||||
<Button
|
||||
as={NextChakraLink}
|
||||
href="https://app.typebot.io/signup"
|
||||
colorScheme="blue"
|
||||
fontWeight="bold"
|
||||
>
|
||||
Create a typebot
|
||||
</Button>
|
||||
</HStack>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export const NavContent = {
|
||||
Mobile: MobileNavContext,
|
||||
Desktop: DesktopNavContent,
|
||||
}
|
48
apps/landing-page/components/common/Navbar/NavLink.tsx
Executable file
48
apps/landing-page/components/common/Navbar/NavLink.tsx
Executable file
@ -0,0 +1,48 @@
|
||||
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,
|
||||
}
|
45
apps/landing-page/components/common/Navbar/NavMenu.tsx
Executable file
45
apps/landing-page/components/common/Navbar/NavMenu.tsx
Executable file
@ -0,0 +1,45 @@
|
||||
import { useColorModeValue } from '@chakra-ui/react'
|
||||
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"
|
||||
bg={useColorModeValue('white', 'gray.700')}
|
||||
w="full"
|
||||
shadow="lg"
|
||||
px="4"
|
||||
pos="absolute"
|
||||
insetX="0"
|
||||
pt="6"
|
||||
pb="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',
|
||||
},
|
||||
},
|
||||
}
|
30
apps/landing-page/components/common/Navbar/Navbar.tsx
Executable file
30
apps/landing-page/components/common/Navbar/Navbar.tsx
Executable file
@ -0,0 +1,30 @@
|
||||
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>
|
||||
)
|
||||
}
|
95
apps/landing-page/components/common/Navbar/Submenu.tsx
Executable file
95
apps/landing-page/components/common/Navbar/Submenu.tsx
Executable file
@ -0,0 +1,95 @@
|
||||
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,
|
||||
}
|
75
apps/landing-page/components/common/Navbar/SubmenuItem.tsx
Executable file
75
apps/landing-page/components/common/Navbar/SubmenuItem.tsx
Executable file
@ -0,0 +1,75 @@
|
||||
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={mode('blue.600', 'blue.400')}
|
||||
>
|
||||
{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>
|
||||
)
|
||||
}
|
65
apps/landing-page/components/common/Navbar/ToggleButton.tsx
Executable file
65
apps/landing-page/components/common/Navbar/ToggleButton.tsx
Executable file
@ -0,0 +1,65 @@
|
||||
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>
|
||||
)
|
||||
}
|
53
apps/landing-page/components/common/Navbar/_data.tsx
Executable file
53
apps/landing-page/components/common/Navbar/_data.tsx
Executable file
@ -0,0 +1,53 @@
|
||||
import { BookIcon } from 'assets/icons/BookIcon'
|
||||
import { DocIcon } from 'assets/icons/DocIcon'
|
||||
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',
|
||||
children: [
|
||||
{
|
||||
label: 'Blog',
|
||||
description:
|
||||
"Content about high-performing forms and guides on how to leverage Typebot's power",
|
||||
href: '/blog',
|
||||
icon: <BookIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Documentation',
|
||||
description:
|
||||
"Everything you need to know about how to use Typebot's builder",
|
||||
href: 'https://docs.typebot.io',
|
||||
icon: <DocIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Roadmap',
|
||||
description:
|
||||
"Follow the development and make suggestions for which features you'd like to see",
|
||||
href: 'https://feedback.typebot.io/roadmap',
|
||||
icon: <MapIcon />,
|
||||
},
|
||||
{
|
||||
label: 'Community',
|
||||
description:
|
||||
'Join our facebook community and get insights on how to create high performing surveys',
|
||||
href: 'https://www.facebook.com/groups/262165102257585',
|
||||
icon: <PeopleCircleIcon />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ label: 'Pricing', href: '/pricing' },
|
||||
]
|
127
apps/landing-page/components/common/Navbar/useNavMenu.ts
Executable file
127
apps/landing-page/components/common/Navbar/useNavMenu.ts
Executable file
@ -0,0 +1,127 @@
|
||||
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,
|
||||
}
|
||||
}
|
34
apps/landing-page/components/common/SocialMetaTags.tsx
Normal file
34
apps/landing-page/components/common/SocialMetaTags.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import Head from 'next/head'
|
||||
import React from 'react'
|
||||
|
||||
export const SocialMetaTags = ({
|
||||
title,
|
||||
description,
|
||||
currentUrl,
|
||||
imagePreviewUrl,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
currentUrl: string
|
||||
imagePreviewUrl: string
|
||||
}) => (
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
|
||||
<meta property="twitter:url" content={currentUrl} />
|
||||
<meta property="og:url" content={currentUrl} />
|
||||
|
||||
<meta name="description" content={description} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="og:description" content={description} />
|
||||
|
||||
<meta property="og:image" content={imagePreviewUrl} />
|
||||
<meta property="twitter:image" content={imagePreviewUrl} />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
</Head>
|
||||
)
|
26
apps/landing-page/components/common/TableCells.tsx
Normal file
26
apps/landing-page/components/common/TableCells.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { CheckIcon } from 'assets/icons/CheckIcon'
|
||||
import { CloseIcon } from 'assets/icons/CloseIcon'
|
||||
import { Td, Text } from '@chakra-ui/react'
|
||||
import React, { ReactNode } from 'react'
|
||||
|
||||
export const Yes = (props: { children?: ReactNode }) => (
|
||||
<Td display={props.children ? 'flex' : ''}>
|
||||
<CheckIcon fill="#0042da" width="25px" />
|
||||
{props.children && (
|
||||
<Text ml={1} fontSize="sm">
|
||||
{props.children}
|
||||
</Text>
|
||||
)}
|
||||
</Td>
|
||||
)
|
||||
|
||||
export const No = (props: { children?: ReactNode }) => (
|
||||
<Td display={props.children ? 'flex' : ''}>
|
||||
<CloseIcon width="25px" />
|
||||
{props.children && (
|
||||
<Text ml={1} fontSize="sm">
|
||||
{props.children}
|
||||
</Text>
|
||||
)}
|
||||
</Td>
|
||||
)
|
@ -0,0 +1,50 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import NextLink from 'next/link'
|
||||
import { LinkProps as NextLinkProps } from 'next/dist/client/link'
|
||||
import {
|
||||
Link as ChakraLink,
|
||||
LinkProps as ChakraLinkProps,
|
||||
} from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
|
||||
export type NextChakraLinkProps = PropsWithChildren<
|
||||
NextLinkProps & Omit<ChakraLinkProps, 'as'>
|
||||
>
|
||||
|
||||
// Has to be a new component because both chakra and next share the `as` keyword
|
||||
// eslint-disable-next-line react/display-name
|
||||
export const NextChakraLink = React.forwardRef(
|
||||
(
|
||||
{
|
||||
href,
|
||||
as,
|
||||
replace,
|
||||
scroll,
|
||||
shallow,
|
||||
prefetch,
|
||||
children,
|
||||
locale,
|
||||
...chakraProps
|
||||
}: NextChakraLinkProps,
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<NextLink
|
||||
passHref={true}
|
||||
href={href}
|
||||
as={as}
|
||||
replace={replace}
|
||||
scroll={scroll}
|
||||
shallow={shallow}
|
||||
prefetch={prefetch}
|
||||
locale={locale}
|
||||
>
|
||||
{/*eslint-disable-next-line @typescript-eslint/ban-ts-comment*/}
|
||||
{/*@ts-ignore*/}
|
||||
<ChakraLink ref={ref} {...chakraProps}>
|
||||
{children}
|
||||
</ChakraLink>
|
||||
</NextLink>
|
||||
)
|
||||
}
|
||||
)
|
Reference in New Issue
Block a user