Initial Commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# `common` dir
|
||||
|
||||
- Common components (maybe reused across the whole app)
|
||||
- Not primitives
|
||||
- Examples: `/media-card.tsx`, `/sidenav.tsx`, `/fade-in-box.tsx`, etc...
|
||||
@@ -0,0 +1,8 @@
|
||||
.transition {
|
||||
position: fixed;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import Image from 'next/image'
|
||||
import * as React from 'react'
|
||||
|
||||
import { DURATION, gsap } from '~/lib/gsap'
|
||||
import { usePageTransition } from '~/lib/gsap/page-transitions'
|
||||
|
||||
import cashSrc from '../../../../public/images/cash-transition.png'
|
||||
import s from './cash-transition.module.scss'
|
||||
|
||||
export const CashTransition = React.memo(() => {
|
||||
const { getTransitionSpace } = usePageTransition()
|
||||
const cashRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
getTransitionSpace(async () => {
|
||||
const tl = gsap.timeline({
|
||||
onComplete: () => {
|
||||
gsap.to(cashRef.current, {
|
||||
top: '100%',
|
||||
duration: DURATION * 2,
|
||||
ease: 'power4.inOut'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const vw = window.innerWidth / 100
|
||||
const vh = window.innerHeight / 100
|
||||
|
||||
const newTop = -1 * Math.max(vw * 12, vh * 20)
|
||||
|
||||
tl.to(cashRef.current, {
|
||||
top: newTop,
|
||||
duration: DURATION * 2.5,
|
||||
ease: 'power4.inOut'
|
||||
})
|
||||
await tl
|
||||
})
|
||||
}, [getTransitionSpace])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cashRef}
|
||||
className={s['transition']}
|
||||
style={{
|
||||
width: 'max(100vw, 100vh)',
|
||||
height: 'calc(120vh + max(12vh, 12vw))'
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={cashSrc}
|
||||
alt="cash"
|
||||
objectFit="cover"
|
||||
layout="fill"
|
||||
objectPosition="top"
|
||||
loading="eager"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
.cursor {
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
max-width: 35px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
|
||||
> span {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import gsap from 'gsap'
|
||||
import Head from 'next/head'
|
||||
import * as React from 'react'
|
||||
|
||||
import { useDeviceDetect } from '~/hooks/use-device-detect'
|
||||
|
||||
import defaultSrc from '../../../../public/images/cursor/default.svg'
|
||||
import defaultActiveSrc from '../../../../public/images/cursor/default-active.svg'
|
||||
import pointerSrc from '../../../../public/images/cursor/pointer.svg'
|
||||
import pointerActiveSrc from '../../../../public/images/cursor/pointer-active.svg'
|
||||
import s from './cursor.module.scss'
|
||||
|
||||
type CursorType = 'pointer' | 'default' | undefined
|
||||
|
||||
const CursorContext = React.createContext<
|
||||
{ setType: React.Dispatch<React.SetStateAction<CursorType>> } | undefined
|
||||
>(undefined)
|
||||
|
||||
const Cursor = ({ children }: { children?: React.ReactNode }) => {
|
||||
const cursorRef = React.useRef<HTMLDivElement>(null)
|
||||
const [type, setType] = React.useState<CursorType>()
|
||||
const { isMobile } = useDeviceDetect()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!cursorRef.current) return
|
||||
gsap.set(cursorRef.current, { xPercent: -50, yPercent: -50 })
|
||||
|
||||
const pos = { x: window.innerWidth / 2, y: window.innerHeight / 2 }
|
||||
const mouse = { x: pos.x, y: pos.y }
|
||||
const speed = 0.2
|
||||
|
||||
const xSet = gsap.quickSetter(cursorRef.current, 'x', 'px')
|
||||
const ySet = gsap.quickSetter(cursorRef.current, 'y', 'px')
|
||||
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
mouse.x = e.x
|
||||
mouse.y = e.y
|
||||
if (e.target instanceof HTMLElement || e.target instanceof SVGElement) {
|
||||
if (e.target.dataset.cursor) {
|
||||
setType(e.target.dataset.cursor as any)
|
||||
return
|
||||
}
|
||||
if (e.target.closest('button') || e.target.closest('a')) {
|
||||
setType('pointer')
|
||||
return
|
||||
} else if (
|
||||
e.target.closest('p') ||
|
||||
e.target.closest('span') ||
|
||||
e.target.closest('h1') ||
|
||||
e.target.closest('h2') ||
|
||||
e.target.closest('h3') ||
|
||||
e.target.closest('h4') ||
|
||||
e.target.closest('h5') ||
|
||||
e.target.closest('h5') ||
|
||||
e.target.closest('input') ||
|
||||
e.target.closest('textarea')
|
||||
) {
|
||||
setType('default') // this would be for text, if we'd have any text cursor
|
||||
return
|
||||
}
|
||||
}
|
||||
setType(undefined)
|
||||
}
|
||||
|
||||
function handleTick() {
|
||||
const dt = 1.0 - Math.pow(0.6 - speed, gsap.ticker.deltaRatio())
|
||||
|
||||
pos.x += (mouse.x - pos.x) * dt
|
||||
pos.y += (mouse.y - pos.y) * dt
|
||||
xSet(pos.x)
|
||||
ySet(pos.y)
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove, { passive: true })
|
||||
gsap.ticker.add(handleTick)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
gsap.ticker.remove(handleTick)
|
||||
}
|
||||
}, [isMobile])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMobile === false && <CursorFollower ref={cursorRef} type={type} />}
|
||||
<CursorContext.Provider value={{ setType }}>
|
||||
{children}
|
||||
</CursorContext.Provider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CursorFollower = React.forwardRef<HTMLDivElement, { type: CursorType }>(
|
||||
({ type }, ref) => {
|
||||
const { src, adjustments } = React.useMemo(() => {
|
||||
switch (type) {
|
||||
case 'pointer':
|
||||
return {
|
||||
src: pointerSrc,
|
||||
adjustments: { x: '4px', y: '22px' }
|
||||
}
|
||||
default:
|
||||
return {
|
||||
src: defaultSrc,
|
||||
adjustments: { x: '10px', y: '17px' }
|
||||
}
|
||||
}
|
||||
}, [type])
|
||||
|
||||
React.useEffect(() => {
|
||||
document.documentElement.classList.add('has-custom-cursor')
|
||||
|
||||
return () => {
|
||||
document.documentElement.classList.remove('has-custom-cursor')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={ref} className={s['cursor']}>
|
||||
<Head>
|
||||
{/* preload images */}
|
||||
{[defaultSrc, defaultActiveSrc, pointerSrc, pointerActiveSrc].map(
|
||||
(src) => {
|
||||
return (
|
||||
<link key={src.src} rel="preload" href={src.src} as="image" />
|
||||
)
|
||||
}
|
||||
)}
|
||||
</Head>
|
||||
<span
|
||||
style={{
|
||||
transform: `translate(${adjustments.x}, ${adjustments.y})`
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={src.src}
|
||||
alt={`cursor-${type}`}
|
||||
width={src.width}
|
||||
height={src.height}
|
||||
loading="eager"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const useCursor = () => {
|
||||
const context = React.useContext(CursorContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useCursor must be used within a CursorProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export default Cursor
|
||||
@@ -0,0 +1,94 @@
|
||||
import { forwardRef, useEffect, useRef, useState } from 'react'
|
||||
import mergeRefs from 'react-merge-refs'
|
||||
|
||||
import { useAnimationContext } from '~/context/animation'
|
||||
import { useIsomorphicLayoutEffect } from '~/hooks/use-isomorphic-layout-effect'
|
||||
import { DURATION, gsap } from '~/lib/gsap'
|
||||
|
||||
type FadeInBoxProps = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
id?: string
|
||||
style?: React.CSSProperties
|
||||
onFadeInCompleteTimeline?: GSAPTimeline
|
||||
}
|
||||
|
||||
export const FadeInBox = forwardRef<HTMLDivElement, FadeInBoxProps>(
|
||||
({ children, className, id, style, onFadeInCompleteTimeline }, ref) => {
|
||||
const innerRef = useRef<HTMLDivElement>(null)
|
||||
const { shouldAnimate } = useAnimationContext()
|
||||
const [inView, setInView] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const elementToObserve = innerRef.current
|
||||
if (!elementToObserve) return
|
||||
const handleObserve: IntersectionObserverCallback = ([element]) => {
|
||||
if (element) {
|
||||
setInView((p) => {
|
||||
// trigger once
|
||||
if (p === true) return true
|
||||
else return element.isIntersecting
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(handleObserve, {
|
||||
threshold: 0.14
|
||||
})
|
||||
|
||||
observer.observe(elementToObserve)
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (!shouldAnimate || !inView) {
|
||||
return
|
||||
}
|
||||
if (!innerRef.current) return
|
||||
|
||||
const tl = gsap.timeline({
|
||||
paused: true,
|
||||
smoothChildTiming: true,
|
||||
defaults: { ease: 'slow', overwrite: true },
|
||||
onComplete: () => {
|
||||
if (
|
||||
onFadeInCompleteTimeline &&
|
||||
typeof onFadeInCompleteTimeline !== 'undefined'
|
||||
) {
|
||||
onFadeInCompleteTimeline.play()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
tl.to(innerRef.current, {
|
||||
autoAlpha: 1,
|
||||
scale: 1,
|
||||
duration: DURATION
|
||||
})
|
||||
|
||||
tl.play()
|
||||
|
||||
return () => {
|
||||
tl.kill()
|
||||
}
|
||||
}, [inView, shouldAnimate, onFadeInCompleteTimeline])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={mergeRefs([innerRef, ref])}
|
||||
className={className}
|
||||
id={id}
|
||||
style={{
|
||||
opacity: shouldAnimate ? 0 : undefined,
|
||||
transform: shouldAnimate ? 'scale(0.9)' : undefined,
|
||||
...style
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,117 @@
|
||||
@import '~/css/helpers';
|
||||
|
||||
.footer {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: tovw(56px, 'default', 48px);
|
||||
border-bottom: tovw(1px, 'default', 1px) solid var(--color-grey-light);
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: tovw(88px, 'default', 64px);
|
||||
@media screen and (max-width: 1024px) {
|
||||
display: grid;
|
||||
column-gap: tovw(97px, 'tablet', 97px);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: auto;
|
||||
row-gap: tovw(44px, 'tablet', 44px);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
|
||||
> li {
|
||||
line-height: 1.35;
|
||||
|
||||
&:first-of-type {
|
||||
line-height: 1;
|
||||
margin-bottom: tovw(12px, 'default', 10px);
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-right: tovw(88px, 'default', 64px);
|
||||
@media screen and (max-width: 1024px) {
|
||||
margin-right: 0;
|
||||
margin-bottom: tovw(56px, 'tablet', 56px);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: tovw(305px, 'default', 120px);
|
||||
@media screen and (max-width: 1024px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.connect__links {
|
||||
> div {
|
||||
display: grid;
|
||||
align-content: flex-start;
|
||||
gap: tovw(16px, 'default', 16px);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: auto;
|
||||
|
||||
li {
|
||||
width: tovw(24px, 'default', 24px);
|
||||
height: tovw(24px, 'default', 24px);
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sub__footer {
|
||||
margin-top: tovw(18px, 'default', 18px);
|
||||
margin-bottom: tovw(44px, 'default', 36px);
|
||||
|
||||
a,
|
||||
p {
|
||||
font-size: tovw(18px, 'default', 14px);
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
color: var(--color-grey-light);
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
@media screen and (max-width: 800px) {
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
li:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
gap: tovw(37px, 'default', 24px);
|
||||
@media screen and (max-width: 800px) {
|
||||
margin-bottom: tovw(32px, 'tablet', 32px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
Facebook,
|
||||
Instagram,
|
||||
Linkedin,
|
||||
Reddit,
|
||||
Telegram,
|
||||
Twitter
|
||||
} from '~/components/icons/socials'
|
||||
|
||||
export const DevelopersLinks = [
|
||||
{ href: '/developers', title: 'Developers' },
|
||||
{ href: '/github', title: 'Github' },
|
||||
{ href: '/roadmap', title: 'Roadmap' },
|
||||
{ href: '/chat', title: 'Chat' },
|
||||
{ href: '/forum', title: 'Forum' }
|
||||
]
|
||||
|
||||
export const ProductsLinks = [
|
||||
{ href: '/products', title: 'Products' },
|
||||
{ href: '/SDK', title: 'SDK' },
|
||||
{ href: '/watchers', title: 'Watchers' },
|
||||
{ href: '/network', title: 'Network' },
|
||||
{ href: '/wallet', title: 'Wallet' },
|
||||
{ href: '/token', title: 'Token' }
|
||||
]
|
||||
|
||||
export const AboutLinks = [
|
||||
{ href: '/about', title: 'About' },
|
||||
{ href: '/faq', title: 'FAQ' },
|
||||
{ href: '/team', title: 'Team' },
|
||||
{ href: '/partners', title: 'Partners' },
|
||||
{ href: '/newsroom', title: 'Newstoom' },
|
||||
{ href: '/careers', title: 'Careers' },
|
||||
{ href: '/contact', title: 'Contact' }
|
||||
]
|
||||
|
||||
export const CommunityLinks = [
|
||||
{ href: '/community', title: 'Community' },
|
||||
{ href: '/validators', title: 'Validators' },
|
||||
{ href: '/testnet', title: 'Testnet' },
|
||||
{ href: '/insiders', title: 'Insiders' }
|
||||
]
|
||||
|
||||
export const ConnectLinks = [
|
||||
{ href: '/community', title: 'Twitter', logo: <Twitter /> },
|
||||
{ href: '/validators', title: 'Telegram', logo: <Telegram /> },
|
||||
{ href: '/testnet', title: 'Reddit', logo: <Reddit /> },
|
||||
{ href: '/insiders', title: 'Linkedin', logo: <Linkedin /> },
|
||||
{ href: '/insiders', title: 'Facebook', logo: <Facebook /> },
|
||||
{ href: '/insiders', title: 'Instagram', logo: <Instagram /> }
|
||||
]
|
||||
@@ -0,0 +1,112 @@
|
||||
import { LogoFooter } from '~/components/icons/logo'
|
||||
import { Container } from '~/components/layout/container'
|
||||
import Link from '~/components/primitives/link'
|
||||
|
||||
import {
|
||||
AboutLinks,
|
||||
CommunityLinks,
|
||||
ConnectLinks,
|
||||
DevelopersLinks,
|
||||
ProductsLinks
|
||||
} from './footer'
|
||||
import s from './footer.module.scss'
|
||||
|
||||
export const Footer = () => {
|
||||
return (
|
||||
<footer className={s.footer}>
|
||||
<Container className={s['container']}>
|
||||
<div className={s['logo']}>
|
||||
<Link variant="unstyled" href="/">
|
||||
<LogoFooter />
|
||||
</Link>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
{DevelopersLinks.map((link) => {
|
||||
return (
|
||||
<li key={link.title}>
|
||||
<Link href={link.href} variant="nav">
|
||||
{link.title}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<ul>
|
||||
{ProductsLinks.map((link) => {
|
||||
return (
|
||||
<li key={link.title}>
|
||||
<Link href={link.href} variant="nav">
|
||||
{link.title}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<ul>
|
||||
{AboutLinks.map((link) => {
|
||||
return (
|
||||
<li key={link.title}>
|
||||
<Link href={link.href} variant="nav">
|
||||
{link.title}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<ul>
|
||||
{CommunityLinks.map((link) => {
|
||||
return (
|
||||
<li key={link.title}>
|
||||
<Link href={link.href} variant="nav">
|
||||
{link.title}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<ul className={s['connect__links']}>
|
||||
<li>
|
||||
<Link href="/connect" variant="nav">
|
||||
Connect
|
||||
</Link>
|
||||
</li>
|
||||
<div>
|
||||
{ConnectLinks.map((link) => {
|
||||
return (
|
||||
<li key={link.title}>
|
||||
<Link href={link.href} variant="unstyled">
|
||||
<span className="sr-only">{link.title}</span>
|
||||
{link.logo && link.logo}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ul>
|
||||
</nav>
|
||||
</Container>
|
||||
<Container>
|
||||
<nav className={s['sub__footer']}>
|
||||
<ul>
|
||||
<div>
|
||||
<li>
|
||||
<Link href="/privacy-policy" variant="nav">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/terms-of-use" variant="nav">
|
||||
Terms of Use
|
||||
</Link>
|
||||
</li>
|
||||
</div>
|
||||
<li>
|
||||
<p>Laconic, The Source of Proof</p>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</Container>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
@import '~/css/helpers';
|
||||
|
||||
.header {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: tovw(14px, 'default', 14px) var(--main-padding-side);
|
||||
background: rgb(4 4 4 / 0.01);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
margin: 0 0 0 tovw(122px, 'default', 90px);
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
gap: tovw(32px, 'default', 20px);
|
||||
}
|
||||
|
||||
.burger {
|
||||
width: tovw(22px, 'default', 22px);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export const defaultHeaderLinks = [
|
||||
{ href: '/', title: 'Home' },
|
||||
{ href: '/developers', title: 'Developers' },
|
||||
{ href: '/products', title: 'Products' },
|
||||
{ href: '/community', title: 'Community' },
|
||||
{ href: '/about', title: 'About' },
|
||||
{ href: '/blog', title: 'Blog' }
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
import clsx from 'clsx'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
import Burger from '~/components/icons/burguer'
|
||||
import { Logo } from '~/components/icons/logo'
|
||||
import { Button } from '~/components/primitives/button'
|
||||
import Link from '~/components/primitives/link'
|
||||
|
||||
import { defaultHeaderLinks } from './header'
|
||||
import s from './header.module.scss'
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<header className={s.header}>
|
||||
<nav>
|
||||
<NextLink href="/">
|
||||
<a>
|
||||
<Logo />
|
||||
</a>
|
||||
</NextLink>
|
||||
<ul className="hide-on-mobile">
|
||||
{defaultHeaderLinks.length > 0 &&
|
||||
defaultHeaderLinks.map((link, index) => (
|
||||
<li key={index}>
|
||||
<Link href={link.href} variant="nav">
|
||||
{link.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<Button size="small" className="hide-on-mobile">
|
||||
Get Started
|
||||
</Button>
|
||||
<Burger className={clsx(s['burger'], 'hide-on-desktop')} />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useRouter } from 'next/dist/client/router'
|
||||
import NextHead from 'next/head'
|
||||
import { NextSeo, NextSeoProps } from 'next-seo'
|
||||
import * as React from 'react'
|
||||
|
||||
import { defaultMeta, siteOrigin } from '~/lib/constants'
|
||||
|
||||
type BasicMeta = {
|
||||
colorScheme?: 'dark' | 'light'
|
||||
description?: string
|
||||
noIndex?: boolean
|
||||
ogImage?: string
|
||||
prefetch?: { href: string; as: string }[]
|
||||
preload?: { href: string; as: string }[]
|
||||
themeColor?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export type MetaProps = BasicMeta & { rawNextSeoProps?: NextSeoProps }
|
||||
|
||||
export const Meta = (props: MetaProps) => {
|
||||
const router = useRouter()
|
||||
|
||||
const nextSeoProps: NextSeoProps = React.useMemo(() => {
|
||||
return {
|
||||
title: props.title ?? defaultMeta.title,
|
||||
description: props.description ?? defaultMeta.description,
|
||||
canonical: `${siteOrigin}${router.pathname}`,
|
||||
openGraph: {
|
||||
images: [
|
||||
{
|
||||
url: props.ogImage ?? defaultMeta.ogImage,
|
||||
alt: props.title ?? defaultMeta.title,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
type: 'image/jpeg'
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
cardType: 'summary_large_image',
|
||||
handle: defaultMeta.twitter.handle,
|
||||
site: defaultMeta.twitter.site
|
||||
},
|
||||
noindex: props.noIndex,
|
||||
...props.rawNextSeoProps
|
||||
}
|
||||
}, [props, router.pathname])
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo {...nextSeoProps} />
|
||||
<NextHead>
|
||||
<meta charSet="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width"
|
||||
/>
|
||||
<meta name="theme-color" content={props.themeColor ?? '#FFB1E4'} />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
|
||||
<meta name="msapplication-TileColor" content="#000000" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
{props.preload?.map(({ href, as }) => (
|
||||
<link key={href} rel="preload" href={href} as={as} />
|
||||
))}
|
||||
{props.prefetch?.map(({ href, as }) => (
|
||||
<link key={href} rel="prefetch" href={href} as={as} />
|
||||
))}
|
||||
</NextHead>
|
||||
|
||||
<style jsx global>{`
|
||||
html {
|
||||
color-scheme: ${props.colorScheme ?? 'light'};
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { useDeviceDetect } from '~/hooks/use-device-detect'
|
||||
|
||||
import s from './noise.module.css'
|
||||
|
||||
export const Noise = ({
|
||||
softLight = true,
|
||||
colorBurn = true,
|
||||
ignoreDevice = false,
|
||||
absolute = false
|
||||
}) => {
|
||||
const { isSafari, isMobile, loaded } = useDeviceDetect()
|
||||
|
||||
if (!loaded) return null
|
||||
return (
|
||||
<>
|
||||
{(ignoreDevice || (!isSafari && !isMobile)) && (
|
||||
<>
|
||||
{softLight && (
|
||||
<div
|
||||
className={clsx(
|
||||
s.noise,
|
||||
'noise',
|
||||
absolute ? 'absolute' : 'fixed'
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
{colorBurn && (
|
||||
<div
|
||||
className={clsx(
|
||||
s.noise2,
|
||||
'noise',
|
||||
absolute ? 'absolute' : 'fixed'
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
.noise {
|
||||
background-color: white;
|
||||
background-image: url('/images/noise.png');
|
||||
background-repeat: repeat;
|
||||
background-size: auto;
|
||||
z-index: 500;
|
||||
width: 300%;
|
||||
height: 300%;
|
||||
left: -100%;
|
||||
top: -100%;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: soft-light;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.noise2 {
|
||||
background-color: white;
|
||||
background-image: url('/images/noise-2.png');
|
||||
background-repeat: repeat;
|
||||
background-size: auto;
|
||||
z-index: 500;
|
||||
width: 300%;
|
||||
height: 300%;
|
||||
left: -100%;
|
||||
top: -100%;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: color-burn;
|
||||
opacity: 0.8;
|
||||
will-change: transform;
|
||||
animation: grain 16s steps(10) infinite;
|
||||
}
|
||||
|
||||
@keyframes grain {
|
||||
0% {
|
||||
transform: translate(20%, -15%);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: translate(-20%, -15%);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translate(20%, -5%);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: translate(-20%, -5%);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translate(20%, 5%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-20%, 5%);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translate(20%, 15%);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: translate(-20%, 15%);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translate(20%, 5%);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: translate(-20%, 5%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(20%, -5%);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
const Arrow = ({ className, fill }: { className?: string; fill?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 23 23"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.758 23H23V3.758l-2.523-.01.01 14.935L1.804 0 0 1.804l18.683 18.683-14.936-.01L3.758 23Z"
|
||||
fill={fill || 'var(--color-white)'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const ArrowDotted = ({
|
||||
className,
|
||||
fill
|
||||
}: {
|
||||
className?: string
|
||||
fill?: string
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 42 14"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m1 35 6 6 6-6" stroke={fill || '#fff'} />
|
||||
<path d="M7 41V0" stroke={fill || '#fff'} strokeDasharray="2 2" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const ArrowLink = ({
|
||||
className,
|
||||
fill
|
||||
}: {
|
||||
className?: string
|
||||
fill?: string
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 11 11"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.455 10.557.427 9.53l7.648-7.662H2.167L2.181.443h8.347v8.36H9.091l.013-5.907-7.649 7.661Z"
|
||||
fill={fill || 'var(--color-white)'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export { Arrow, ArrowDotted, ArrowLink }
|
||||
@@ -0,0 +1,18 @@
|
||||
const Burger = ({ className, fill }: { className?: string; fill?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 22 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke={fill || '#fff'}
|
||||
style={{ mixBlendMode: 'difference' }}
|
||||
d="M0 .5h22M0 5.5h22M0 10.5h22"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default Burger
|
||||
@@ -0,0 +1,72 @@
|
||||
const Isotype = ({
|
||||
className,
|
||||
fill
|
||||
}: {
|
||||
className?: string
|
||||
fill?: string
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="94"
|
||||
height="95"
|
||||
viewBox="0 0 94 95"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.86 49.967C26.8 38.911 33.57 23.643 33.568 6.785A60.4 60.4 0 0 0 33.198 0L0 .003l.001 64.465c-.003 7.814 2.945 15.631 8.842 21.59 5.897 5.96 13.637 8.942 21.37 8.938l-.002.002L94 95l-.002-33.556a60.653 60.653 0 0 0-6.714-.373c-16.676.002-31.784 6.845-42.724 17.9-7.96 7.844-20.724 7.845-28.586-.1-7.858-7.941-7.86-20.845-.114-28.904ZM87.114 6.976c-9.168-9.265-24.063-9.269-33.234 0-9.171 9.268-9.168 24.321 0 33.586 9.173 9.27 24.063 9.269 33.234 0 9.171-9.268 9.173-24.316 0-33.586Z"
|
||||
fill={fill || 'var(--color-white)'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const Logo = ({ className, fill }: { className?: string; fill?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="133"
|
||||
height="24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M37.761 22.302h9.246v-2.704h-6.155v-17.9h-3.09v20.604ZM59.314 1.697h-5.126l-5.357 20.605h3.194l1.34-5.151h6.618l1.34 5.151h3.348L59.314 1.697Zm-5.306 12.878 2.679-10.663h.103l2.575 10.663h-5.357ZM74.337 9.682h3.606c0-5.873-1.88-8.397-6.259-8.397-4.61 0-6.593 3.194-6.593 10.689 0 7.52 1.983 10.74 6.593 10.74 4.379 0 6.259-2.447 6.285-8.139h-3.606c-.026 4.456-.567 5.563-2.679 5.563-2.42 0-3.013-1.622-2.987-8.164 0-6.516.592-8.14 2.987-8.113 2.112 0 2.653 1.159 2.653 5.82ZM86.689 1.285c4.687.026 6.696 3.245 6.696 10.715 0 7.469-2.009 10.688-6.696 10.714-4.714.026-6.723-3.194-6.723-10.714 0-7.521 2.01-10.74 6.723-10.715ZM83.572 12c0 6.516.618 8.139 3.117 8.139 2.472 0 3.09-1.623 3.09-8.14 0-6.541-.618-8.164-3.09-8.138-2.499.026-3.117 1.648-3.117 8.139ZM99.317 22.276l-3.09.026V1.697h5.434l5.074 16.793h.052V1.697h3.09v20.605h-5.099l-5.409-18.08h-.052v18.054ZM116.615 1.697h-3.091v20.605h3.091V1.697ZM128.652 9.682h3.606c0-5.873-1.881-8.397-6.259-8.397-4.61 0-6.594 3.194-6.594 10.689 0 7.52 1.984 10.74 6.594 10.74 4.378 0 6.259-2.447 6.284-8.139h-3.605c-.026 4.456-.567 5.563-2.679 5.563-2.421 0-3.014-1.622-2.988-8.164 0-6.516.593-8.14 2.988-8.113 2.112 0 2.653 1.159 2.653 5.82Z"
|
||||
fill={fill || 'var(--color-white)'}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.05 12.623A15.378 15.378 0 0 0 8.57 1.714C8.573 1.136 8.54.564 8.477 0H0v16.287c0 1.974.752 3.949 2.258 5.454A7.69 7.69 0 0 0 7.714 24L24 24v-8.477a15.636 15.636 0 0 0-1.715-.095c-4.258 0-8.115 1.73-10.908 4.523-2.032 1.981-5.291 1.982-7.299-.026-2.006-2.006-2.007-5.266-.029-7.302Zm18.192-10.86a6.004 6.004 0 0 0-8.485 0 6.003 6.003 0 0 0 0 8.484 6.003 6.003 0 0 0 8.485 0 6.002 6.002 0 0 0 0-8.485Z"
|
||||
fill={fill || 'var(--color-white)'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const LogoFooter = ({
|
||||
className,
|
||||
fill
|
||||
}: {
|
||||
className?: string
|
||||
fill?: string
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 305 70"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 68.656h29.794v-8.834H9.96V1.35H0v67.306ZM69.388 1.35H52.873L35.61 68.656H45.9l4.316-16.826h21.33l4.315 16.826H86.65L69.388 1.35ZM52.292 43.416l8.631-34.83h.332l8.3 34.83H52.291ZM118.076 27.427h11.619C129.695 8.245 123.636 0 109.528 0 94.672 0 88.282 10.432 88.282 34.915c0 24.567 6.39 35.084 21.246 35.084 14.108 0 20.167-7.993 20.25-26.586h-11.619c-.083 14.555-1.826 18.173-8.631 18.173-7.802 0-9.71-5.3-9.627-26.67 0-21.286 1.908-26.587 9.627-26.503 6.805 0 8.548 3.786 8.548 19.014ZM158.069 0c15.105.085 21.578 10.601 21.578 35s-6.473 34.915-21.578 35c-15.188.084-21.661-10.433-21.661-35s6.473-35.084 21.661-35Zm-10.042 35c0 21.286 1.992 26.586 10.042 26.586 7.967 0 9.959-5.3 9.959-26.586 0-21.37-1.992-26.67-9.959-26.586-8.05.084-10.042 5.384-10.042 26.586ZM198.74 68.572l-9.959.084V1.35h17.511l16.35 54.855h.166V1.35h9.959v67.306h-16.432L198.906 9.595h-.166v58.977ZM254.391 1.35h-9.959v67.306h9.959V1.35ZM293.298 27.427h11.619C304.917 8.245 298.859 0 284.75 0c-14.856 0-21.246 10.432-21.246 34.915 0 24.567 6.39 35.084 21.246 35.084 14.109 0 20.167-7.993 20.25-26.586h-11.619c-.083 14.555-1.826 18.173-8.631 18.173-7.801 0-9.71-5.3-9.627-26.67 0-21.286 1.909-26.587 9.627-26.503 6.805 0 8.548 3.786 8.548 19.014Z"
|
||||
fill={fill || 'var(--color-white)'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export { Isotype, Logo, LogoFooter }
|
||||
@@ -0,0 +1,228 @@
|
||||
const Telegram = ({
|
||||
className,
|
||||
fill
|
||||
}: {
|
||||
className?: string
|
||||
fill?: string
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M24 12c0 6.6274-5.3726 12-12 12-6.62742 0-12-5.3726-12-12C0 5.37258 5.37258 0 12 0c6.6274 0 12 5.37258 12 12ZM12.43 8.85893c-1.1672.48547-3.49986 1.49027-6.99811 3.01437-.56806.2259-.86563.4469-.89272.663-.04578.3652.41154.509 1.0343.7048.08471.0267.17248.0543.26247.0835.6127.1992 1.43689.4322 1.86535.4414.38865.0084.82244-.1518 1.30135-.4807 3.26856-2.2063 4.95576-3.32149 5.06166-3.34553.0747-.01696.1783-.03829.2485.02408.0701.06235.0632.18045.0558.21215-.0453.1931-1.8405 1.8621-2.7695 2.7258-.2896.2692-.495.4602-.537.5038-.0941.0977-.19.1902-.2821.279-.5692.5487-.99609.9602.0236 1.6322.49.3229.8822.5899 1.2733.8563.4273.291.8534.5812 1.4047.9426.1405.092.2746.1877.4053.2808.4972.3545.9438.6729 1.4957.6221.3206-.0295.6519-.331.8201-1.2302.3975-2.1253 1.1789-6.7299 1.3595-8.62743.0158-.16624-.0041-.379-.02-.4724-.016-.09339-.0494-.22646-.1708-.32497-.1438-.11666-.3658-.14126-.465-.13952-.4514.00795-1.1438.24874-4.4764 1.63485Z"
|
||||
fill="url(#telegram)"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="telegram"
|
||||
x1="12"
|
||||
y1="0"
|
||||
x2="12"
|
||||
y2="24"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor={fill || 'var(--color-white)'} />
|
||||
<stop offset="1" stopColor="#DEDEDE" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const Twitter = ({
|
||||
className,
|
||||
fill
|
||||
}: {
|
||||
className?: string
|
||||
fill?: string
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.55016 21.3548c9.05434 0 14.00814-7.4471 14.00814-13.9033 0-.20936-.0047-.42337-.0141-.63273.9637-.69168 1.7953-1.54843 2.4558-2.52999-.8975.39631-1.8504.65515-2.8261.76765 1.0274-.61122 1.7966-1.57142 2.1652-2.7026-.9665.56851-2.0235.96954-3.1257 1.18591-.7426-.78315-1.7244-1.30169-2.7937-1.47544-1.0693-.17376-2.1665.00695-3.122.51417-.9554.50722-1.7159 1.31271-2.1639 2.29194-.4479.97922-.5584 2.07764-.3143 3.12543-1.95701-.09747-3.87156-.60206-5.61952-1.48104-1.74795-.87898-3.29029-2.11274-4.52701-3.62129-.62857 1.07562-.820913 2.34843-.53794 3.55975C1.418 7.66457 2.15506 8.7235 3.19641 9.41483c-.78178-.02463-1.54643-.23354-2.230785-.60947v.06048c-.0007 1.12879.392475 2.22296 1.112685 3.09656.72021.8736 1.72301 1.4727 2.83794 1.6955-.72419.1966-1.48427.2253-2.22141.0837.31461.9708.92673 1.8198 1.75093 2.4287.8242.6088 1.81935.9471 2.84657.9676-1.74392 1.3596-3.89817 2.0971-6.11578 2.0936C.783287 19.2309.390399 19.2069 0 19.1598c2.25286 1.4345 4.87353 2.1964 7.55016 2.195Z"
|
||||
fill="url(#twitter)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="twitter"
|
||||
x1="12"
|
||||
y1="2"
|
||||
x2="12"
|
||||
y2="21.3548"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor={fill || 'var(--color-white)'} />
|
||||
<stop offset="1" stopColor="#DEDEDE" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const Reddit = ({ className, fill }: { className?: string; fill?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M24 12c0 6.6274-5.3726 12-12 12-6.62742 0-12-5.3726-12-12C0 5.37258 5.37258 0 12 0c6.6274 0 12 5.37258 12 12Zm-5.7544-1.7544C19.214 10.2456 20 11.0316 20 12c0 .7158-.4351 1.3333-1.0105 1.614.0281.1684.0421.3369.0421.5193 0 2.6947-3.1298 4.8702-7.0035 4.8702-3.87371 0-7.00353-2.1755-7.00353-4.8702 0-.1824.01403-.3649.0421-.5333-.61754-.2807-1.03859-.8842-1.03859-1.6 0-.9684.78596-1.7544 1.75438-1.7544.46316 0 .89825.1965 1.20702.4912 1.20702-.88419 2.87719-1.43156 4.74382-1.4877l.8843-4.18246c.028-.08421.0701-.15438.1403-.19649.0702-.0421.1544-.05614.2386-.0421l2.9053.61754c.1965-.42105.6175-.70175 1.1087-.70175.6878 0 1.2492.5614 1.2492 1.24912s-.5614 1.24912-1.2492 1.24912c-.6736 0-1.221-.53333-1.2491-1.19298l-2.5965-.54737-.8 3.74737c1.8246.07018 3.4807.63158 4.6737 1.4877.3088-.3088.7298-.4912 1.207-.4912ZM9.24913 12c-.68772 0-1.24912.5614-1.24912 1.2491 0 .6877.5614 1.2491 1.24912 1.2491s1.24917-.5614 1.24917-1.2491c0-.6877-.56145-1.2491-1.24917-1.2491Zm2.76487 5.4596c.4772 0 2.1053-.0561 2.9614-.9123.1264-.1263.1264-.3228.0281-.4631-.1263-.1263-.3368-.1263-.4631 0-.5474.5333-1.6843.7298-2.5123.7298-.8281 0-1.979-.1965-2.5123-.7298-.12632-.1263-.33685-.1263-.46316 0-.12632.1263-.12632.3368 0 .4631.8421.8422 2.48416.9123 2.96136.9123Zm1.4878-4.2105c0 .6877.5614 1.2491 1.2491 1.2491.6877 0 1.2491-.5614 1.2491-1.2491C16 12.5614 15.4386 12 14.7509 12c-.6877 0-1.2491.5614-1.2491 1.2491Z"
|
||||
fill="url(#reddit)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="reddit"
|
||||
x1="12"
|
||||
y1="0"
|
||||
x2="12"
|
||||
y2="24"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor={fill || 'var(--color-white)'} />
|
||||
<stop offset="1" stopColor="#DEDEDE" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const Linkedin = ({
|
||||
className,
|
||||
fill
|
||||
}: {
|
||||
className?: string
|
||||
fill?: string
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3e-7 2.00509C3e-7 1.47331.21125.963308.587278.58728.963305.211253 1.47331.00000267 2.00509.00000267H21.9927c.2636-.00043042.5246.05112313.7682.15170933.2436.100586.4649.248229.6514.434476.1864.186247.3343.40744.4352.650912.1008.24348.1526.50446.1525.76799V21.9927c.0003.2636-.0514.5247-.1521.7683-.1007.2436-.2485.4649-.4348.6513-.1863.1865-.4076.3343-.6511.4352-.2436.1008-.5046.1526-.7682.1525H2.00509c-.2634 0-.52423-.0519-.76757-.1527-.243336-.1009-.464424-.2487-.650628-.435-.186204-.1863-.333875-.4074-.434576-.6508-.1007015-.2434-.15245901-.5043-.1523157-.7677V2.00509ZM9.49964 9.15055h3.24986v1.63195C13.2185 9.84437 14.4185 9 16.2218 9c3.4571 0 4.2764 1.8687 4.2764 5.2975v6.3512h-3.4986v-5.5702c0-1.9527-.4691-3.0545-1.6603-3.0545-1.6528 0-2.34 1.188-2.34 3.0545v5.5702H9.49964V9.15055Zm-6 11.34875h3.49963V9H3.49964v11.4993ZM7.5 5.24946c.0066.29964-.04672.59758-.15683.87634s-.27479.53271-.48437.74696c-.20959.21426-.45986.38449-.73612.5007-.27627.11622-.57296.17609-.87268.17609s-.59641-.05987-.87268-.17609c-.27626-.11621-.52653-.28644-.73612-.5007-.20958-.21425-.37426-.4682-.48437-.74696C3.04672 5.84704 2.9934 5.5491 3 5.24946c.01295-.58816.25569-1.14787.67624-1.55925.42054-.41139.98546-.64175 1.57376-.64175s1.15322.23036 1.57376.64175c.42055.41138.66329.97109.67624 1.55925Z"
|
||||
fill={fill || 'var(--color-white)'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const Facebook = ({
|
||||
className,
|
||||
fill
|
||||
}: {
|
||||
className?: string
|
||||
fill?: string
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M24 12c0-6.62742-5.3726-12-12-12C5.37258 0 0 5.37258 0 12c0 5.9895 4.3882 10.954 10.125 11.8542v-8.3855H7.07812V12H10.125V9.35625c0-3.0075 1.7916-4.66875 4.5326-4.66875 1.3125 0 2.6862.23437 2.6862.23437V7.875h-1.5132c-1.4906 0-1.9556.92508-1.9556 1.875V12h3.3281l-.532 3.4687H13.875v8.3855C19.6118 22.954 24 17.9895 24 12Z"
|
||||
fill="url(#facebook)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="facebook"
|
||||
x1="12"
|
||||
y1="0"
|
||||
x2="12"
|
||||
y2="23.8542"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor={fill || 'var(--color-white)'} />
|
||||
<stop offset="1" stopColor="#DEDEDE" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const Instagram = ({
|
||||
className,
|
||||
fill
|
||||
}: {
|
||||
className?: string
|
||||
fill?: string
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 2.16094c3.2063 0 3.5859.01406 4.8469.07031 1.1719.05156 1.8047.24844 2.2265.4125.5579.21563.961.47813 1.3782.89531.4218.42188.6797.82032.8953 1.37813.164.42187.3609 1.05937.4125 2.22656.0562 1.26562.0703 1.64531.0703 4.84685 0 3.2063-.0141 3.586-.0703 4.8469-.0516 1.1719-.2485 1.8047-.4125 2.2266-.2156.5578-.4782.9609-.8953 1.3781-.4219.4219-.8203.6797-1.3782.8953-.4218.1641-1.0593.3609-2.2265.4125-1.2656.0562-1.6453.0703-4.8469.0703-3.20625 0-3.58594-.0141-4.84687-.0703-1.17188-.0516-1.80469-.2484-2.22657-.4125-.55781-.2156-.96093-.4781-1.37812-.8953-.42188-.4219-.67969-.8203-.89531-1.3781-.16407-.4219-.36094-1.0594-.4125-2.2266-.05625-1.2656-.07032-1.6453-.07032-4.8469 0-3.20622.01407-3.58591.07032-4.84685.05156-1.17188.24843-1.80469.4125-2.22656.21562-.55781.47812-.96094.89531-1.37813.42187-.42187.82031-.67968 1.37812-.89531.42188-.16406 1.05938-.36094 2.22657-.4125C8.41406 2.175 8.79375 2.16094 12 2.16094ZM12 0C8.74219 0 8.33438.0140625 7.05469.0703125 5.77969.126563 4.90313.332812 4.14375.628125 3.35156.9375 2.68125 1.34531 2.01563 2.01562 1.34531 2.68125.9375 3.35156.628125 4.13906.332812 4.90313.126563 5.775.0703125 7.05.0140625 8.33437 0 8.74219 0 12c0 3.2578.0140625 3.6656.0703125 4.9453.0562505 1.275.2624995 2.1516.5578125 2.911.309375.7921.717185 1.4625 1.387505 2.1281.66562.6656 1.33593 1.0781 2.12343 1.3828.76407.2953 1.63594.5015 2.91094.5578 1.27969.0562 1.6875.0703 4.9453.0703 3.2578 0 3.6656-.0141 4.9453-.0703 1.275-.0563 2.1516-.2625 2.911-.5578.7875-.3047 1.4578-.7172 2.1234-1.3828.6656-.6656 1.0781-1.336 1.3828-2.1235.2953-.764.5016-1.6359.5578-2.9109.0563-1.2797.0703-1.6875.0703-4.9453 0-3.25782-.014-3.66564-.0703-4.94532-.0562-1.275-.2625-2.15157-.5578-2.91094-.2953-.79688-.7031-1.46719-1.3734-2.13282C21.3188 1.35 20.6484.9375 19.8609.632812 19.0969.3375 18.225.13125 16.95.075 15.6656.0140625 15.2578 0 12 0Z"
|
||||
fill="url(#instagram-a)"
|
||||
/>
|
||||
<path
|
||||
d="M12 5.83594c-3.40312 0-6.16406 2.76094-6.16406 6.16406 0 3.4031 2.76094 6.1641 6.16406 6.1641 3.4031 0 6.1641-2.761 6.1641-6.1641 0-3.40312-2.761-6.16406-6.1641-6.16406Zm0 10.16246c-2.20781 0-3.99844-1.7906-3.99844-3.9984 0-2.20781 1.79063-3.99844 3.99844-3.99844 2.2078 0 3.9984 1.79063 3.9984 3.99844 0 2.2078-1.7906 3.9984-3.9984 3.9984Z"
|
||||
fill="url(#instagram-b)"
|
||||
/>
|
||||
<path
|
||||
d="M19.8469 5.59238c0 .79688-.6469 1.43907-1.4391 1.43907-.7969 0-1.439-.64688-1.439-1.43907 0-.79687.6468-1.43906 1.439-1.43906s1.4391.64688 1.4391 1.43906Z"
|
||||
fill="url(#instagram-c)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="instagram-a"
|
||||
x1="11.993"
|
||||
y1="0"
|
||||
x2="11.993"
|
||||
y2="23.9953"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor={fill || 'var(--color-white)'} />
|
||||
<stop offset="1" stopColor="#DEDEDE" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="instagram-b"
|
||||
x1="12"
|
||||
y1="5.83594"
|
||||
x2="12"
|
||||
y2="18.1641"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor={fill || 'var(--color-white)'} />
|
||||
<stop offset="1" stopColor="#DEDEDE" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="instagram-c"
|
||||
x1="18.4078"
|
||||
y1="4.15332"
|
||||
x2="18.4078"
|
||||
y2="7.03145"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor={fill || 'var(--color-white)'} />
|
||||
<stop offset="1" stopColor="#DEDEDE" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export { Facebook, Instagram, Linkedin, Reddit, Telegram, Twitter }
|
||||
@@ -0,0 +1,28 @@
|
||||
.aspect-box {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@supports (aspect-ratio: var(--raw-ratio)) {
|
||||
.aspect-box {
|
||||
aspect-ratio: var(--raw-ratio);
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@supports not (aspect-ratio: var(--raw-ratio)) {
|
||||
.aspect-box::before {
|
||||
content: '';
|
||||
width: 1px;
|
||||
margin-left: -1px;
|
||||
float: left;
|
||||
height: 0;
|
||||
padding-top: var(--ratio);
|
||||
}
|
||||
|
||||
.aspect-box::after {
|
||||
/* to clear float */
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import clsx from 'clsx'
|
||||
import * as React from 'react'
|
||||
|
||||
import s from './aspect-box.module.css'
|
||||
|
||||
export const AspectBox = ({
|
||||
ratio,
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
...rest
|
||||
}: { ratio: number } & JSX.IntrinsicElements['div']) => {
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
className={clsx(s['aspect-box'], className)}
|
||||
style={{
|
||||
...style,
|
||||
['--ratio' as string]: `${100 / ratio}%`,
|
||||
['--raw-ratio' as string]: ratio
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
@import '~/css/helpers';
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: tovw(1296px, 'default', 320px);
|
||||
margin: 0 auto;
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
max-width: 100%;
|
||||
padding: 0 tovw(27px, 'tablet', 27px);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import clsx from 'clsx'
|
||||
import * as React from 'react'
|
||||
|
||||
import s from './container.module.scss'
|
||||
|
||||
export const Container = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
JSX.IntrinsicElements['div']
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div {...props} className={clsx(s['container'], className)} ref={ref} />
|
||||
)
|
||||
})
|
||||
|
||||
export type ContainerProps = React.ComponentProps<typeof Container>
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { Footer } from '~/components/common/footer'
|
||||
import { Header } from '~/components/common/header'
|
||||
import { LocomotiveScrollProvider } from '~/lib/locomotive-scroll/provider'
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode
|
||||
extras?: React.ReactNode
|
||||
locomotiveScroll?: boolean
|
||||
}
|
||||
|
||||
const ContentMemo = React.memo(({ children, extras }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{extras}
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export const PageLayout = (props: Props) => {
|
||||
if (props.locomotiveScroll) {
|
||||
return (
|
||||
<LocomotiveScrollProvider>
|
||||
<ContentMemo {...props} />
|
||||
</LocomotiveScrollProvider>
|
||||
)
|
||||
} else {
|
||||
return <ContentMemo {...props} />
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import clsx from 'clsx'
|
||||
import gsap from 'gsap'
|
||||
import * as React from 'react'
|
||||
import mergeRefs from 'react-merge-refs'
|
||||
|
||||
import { useAnimationContext } from '~/context/animation'
|
||||
import { useIsomorphicLayoutEffect } from '~/hooks/use-isomorphic-layout-effect'
|
||||
|
||||
import s from './section.module.scss'
|
||||
|
||||
type SectionProps = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
disableFadeIn?: boolean
|
||||
id?: string
|
||||
onBlur?: React.FocusEventHandler
|
||||
scrollSection?: boolean
|
||||
timeline?: GSAPTimeline
|
||||
variant?: 'default' | 'stripes' | 'stripes-center' | 'unstyled'
|
||||
}
|
||||
|
||||
const Section = React.forwardRef<HTMLDivElement, SectionProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
disableFadeIn,
|
||||
id,
|
||||
onBlur,
|
||||
scrollSection,
|
||||
timeline,
|
||||
variant
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const innerRef = React.useRef<HTMLDivElement>(null)
|
||||
const { shouldAnimate } = useAnimationContext()
|
||||
const [inView, setInView] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const elementToObserve = innerRef.current
|
||||
if (!elementToObserve) return
|
||||
const handleObserve: IntersectionObserverCallback = ([element]) => {
|
||||
if (element) {
|
||||
setInView((p) => {
|
||||
// trigger once
|
||||
if (p === true) return true
|
||||
else return element.isIntersecting
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(handleObserve, {
|
||||
rootMargin: '8% 0%'
|
||||
})
|
||||
|
||||
observer.observe(elementToObserve)
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (disableFadeIn || !shouldAnimate || !inView || !innerRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const tl = gsap.timeline({
|
||||
paused: true,
|
||||
smoothChildTiming: true,
|
||||
onComplete: () => {
|
||||
if (timeline) {
|
||||
timeline.play()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
tl.from(innerRef.current, { autoAlpha: 0 })
|
||||
tl.play()
|
||||
|
||||
return () => {
|
||||
tl.kill()
|
||||
}
|
||||
}, [inView, shouldAnimate, timeline, disableFadeIn])
|
||||
|
||||
return (
|
||||
<section
|
||||
className={clsx(className, s.base, variant && s[`variant-${variant}`])}
|
||||
id={id}
|
||||
ref={mergeRefs([innerRef, ref])}
|
||||
onBlur={onBlur}
|
||||
{...(scrollSection ? { 'data-scroll-section': true } : {})}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default Section
|
||||
@@ -0,0 +1,30 @@
|
||||
@import '~/css/helpers';
|
||||
|
||||
.base:not(.variant-unstyled) {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.variant-stripes,
|
||||
.variant-stripes-center {
|
||||
background-color: theme('colors.pink');
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stripesBase {
|
||||
top: tovw(-524px);
|
||||
left: tovw(-342px);
|
||||
width: tovw(2605px, 'desktop-large');
|
||||
height: tovw(2460px, 'desktop-large');
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.children {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
@import '~/css/helpers';
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
transition: all var(--normal-transition);
|
||||
border: tovw(1px) solid var(--color-white);
|
||||
border-radius: tovw(4px);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
padding: tovw(16px, 'default', 10px) tovw(32px, 'default', 14px);
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
color: var(--color-white);
|
||||
font-family: var(--font-dm-mono);
|
||||
font-size: tovw(18px, 'default', 14px);
|
||||
appearance: none;
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
&--primary {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 tovw(50px, 'default', 16px) var(--color-accent);
|
||||
background: var(--color-accent);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 var(--color-accent);
|
||||
background: var(--color-accent);
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
||||
|
||||
&--small {
|
||||
padding: tovw(11.5px, 'default', 11.5px) tovw(24px, 'default', 10px);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type * as Polymorphic from '@radix-ui/react-polymorphic'
|
||||
import clsx from 'clsx'
|
||||
import Link, { LinkProps } from 'next/link'
|
||||
import * as React from 'react'
|
||||
|
||||
import { checkIsExternal } from '~/lib/utils/router'
|
||||
|
||||
import s from './button.module.scss'
|
||||
|
||||
export type ButtonProps = JSX.IntrinsicElements['button'] & {
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
variant?: 'default' | 'primary'
|
||||
}
|
||||
|
||||
export const Button = React.forwardRef(
|
||||
(
|
||||
{
|
||||
as: Comp = 'button',
|
||||
children,
|
||||
className,
|
||||
size = 'medium',
|
||||
variant = 'default',
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<Comp
|
||||
className={clsx(
|
||||
s.button,
|
||||
className,
|
||||
s[`button--${size}`],
|
||||
s[`button--${variant}`]
|
||||
)}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
)
|
||||
}
|
||||
) as Polymorphic.ForwardRefComponent<'button', ButtonProps>
|
||||
|
||||
type NextLinkProps = Pick<
|
||||
LinkProps,
|
||||
'href' | 'locale' | 'prefetch' | 'replace' | 'scroll' | 'shallow'
|
||||
>
|
||||
|
||||
export type ButtonLinkProps = ButtonProps &
|
||||
Omit<JSX.IntrinsicElements['a'], 'href'> &
|
||||
NextLinkProps & { notExternal?: boolean }
|
||||
|
||||
export const ButtonLink = React.forwardRef<'a', ButtonLinkProps>(
|
||||
(
|
||||
{
|
||||
// NextLinkProps
|
||||
href,
|
||||
replace,
|
||||
scroll = false,
|
||||
shallow,
|
||||
prefetch,
|
||||
locale,
|
||||
// Rest
|
||||
notExternal,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const externalProps = React.useMemo(() => {
|
||||
const p = { target: '_blank', rel: 'noopener' }
|
||||
if (typeof href === 'string') {
|
||||
if (checkIsExternal(href)) return p
|
||||
} else if (checkIsExternal(href.href ?? '')) return p
|
||||
}, [href])
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
replace={replace}
|
||||
scroll={scroll}
|
||||
shallow={shallow}
|
||||
prefetch={prefetch}
|
||||
locale={locale}
|
||||
passHref
|
||||
>
|
||||
<Button
|
||||
{...(notExternal ? undefined : externalProps)}
|
||||
{...props}
|
||||
as="a"
|
||||
// @ts-ignore
|
||||
ref={ref}
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default Button
|
||||
@@ -0,0 +1,51 @@
|
||||
@import 'css/helpers';
|
||||
|
||||
.heading {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 400;
|
||||
font-kerning: none;
|
||||
|
||||
&.centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&--xl {
|
||||
font-size: tovw(110px, 'default', 48px);
|
||||
line-height: 1.1;
|
||||
letter-spacing: tovw(-3px);
|
||||
}
|
||||
|
||||
&--lg {
|
||||
font-size: tovw(76px, 'default', 42px);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&--md {
|
||||
font-size: tovw(58px, 'default', 42px);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
&--sm {
|
||||
font-size: tovw(40px, 'default', 36px);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&--xs {
|
||||
font-size: tovw(12px, 'default', 12px);
|
||||
line-height: 1.3;
|
||||
letter-spacing: tovw(-0.5px);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&--arthemys {
|
||||
font-family: var(--font-arthemys);
|
||||
}
|
||||
|
||||
&--tthoves {
|
||||
font-family: var(--font-tt-hoves);
|
||||
}
|
||||
|
||||
&--dmmono {
|
||||
font-family: var(--font-dm-mono);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type * as Polymorphic from '@radix-ui/react-polymorphic'
|
||||
import clsx from 'clsx'
|
||||
import * as React from 'react'
|
||||
|
||||
// Styles
|
||||
import s from './heading.module.scss'
|
||||
|
||||
type HeadingProps = {
|
||||
variant: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
font?: 'arthemys' | 'tthoves' | 'dmmono'
|
||||
centered?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_ELEMENT = 'h2'
|
||||
|
||||
const Heading = React.forwardRef(
|
||||
(
|
||||
{
|
||||
as: Comp = DEFAULT_ELEMENT,
|
||||
centered = false,
|
||||
children,
|
||||
className,
|
||||
font,
|
||||
variant,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<Comp
|
||||
className={clsx(
|
||||
s.heading,
|
||||
s[`heading--${variant}`],
|
||||
s[`heading--${font}`],
|
||||
{ [s['centered']]: centered },
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
)
|
||||
}
|
||||
) as Polymorphic.ForwardRefComponent<typeof DEFAULT_ELEMENT, HeadingProps>
|
||||
|
||||
export default Heading
|
||||
@@ -0,0 +1,3 @@
|
||||
.highlight {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
import s from './highlighted-text.module.scss'
|
||||
|
||||
const HighlightedText: FC = ({ children }) => {
|
||||
return <span className={s['highlight']}>{children}</span>
|
||||
}
|
||||
|
||||
export default HighlightedText
|
||||
@@ -0,0 +1,64 @@
|
||||
import clsx from 'clsx'
|
||||
import { checkIsExternal } from 'lib/utils/router'
|
||||
import NextLink, { LinkProps as NextLinkProps } from 'next/link'
|
||||
import * as React from 'react'
|
||||
|
||||
import { ArrowLink } from '~/components/icons/arrow'
|
||||
|
||||
import s from './link.module.scss'
|
||||
|
||||
export type LinkProps = {
|
||||
children?: React.ReactNode
|
||||
variant?: 'default' | 'nav' | 'unstyled'
|
||||
} & JSX.IntrinsicElements['a'] &
|
||||
Omit<NextLinkProps, 'as' | 'passHref'>
|
||||
|
||||
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
|
||||
({ children, className, variant = 'default', ...restProps }, ref) => {
|
||||
const {
|
||||
href,
|
||||
// NextLink Props
|
||||
replace,
|
||||
scroll,
|
||||
shallow,
|
||||
prefetch,
|
||||
// Rest
|
||||
...aProps
|
||||
} = restProps
|
||||
|
||||
const isExternal = checkIsExternal(href)
|
||||
|
||||
return (
|
||||
<NextLink
|
||||
href={href}
|
||||
replace={replace}
|
||||
scroll={scroll}
|
||||
shallow={shallow}
|
||||
prefetch={prefetch}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
className={clsx(
|
||||
variant !== 'unstyled' && s['link'],
|
||||
variant !== 'unstyled' && s[`link--${variant}`],
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
rel={isExternal ? 'noopener' : undefined}
|
||||
target={isExternal ? '_blank' : undefined}
|
||||
{...aProps}
|
||||
>
|
||||
{children}
|
||||
{variant === 'default' &&
|
||||
(isExternal ? (
|
||||
<ArrowLink className={s['icon']} />
|
||||
) : (
|
||||
<ArrowLink className={clsx(s['icon'], s['icon--rotated'])} />
|
||||
))}
|
||||
</a>
|
||||
</NextLink>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default Link
|
||||
@@ -0,0 +1,161 @@
|
||||
@import 'css/helpers';
|
||||
|
||||
.link {
|
||||
font-family: var(--font-dm-mono);
|
||||
font-size: tovw(18px, 'default', 14px);
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
transition: color var(--duration-fast) var(--ease);
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
pointer-events: all;
|
||||
color: currentcolor;
|
||||
outline: none;
|
||||
|
||||
&--default,
|
||||
&--nav {
|
||||
.icon {
|
||||
position: absolute;
|
||||
right: tovw(-20px, 'default', -18px);
|
||||
bottom: tovw(7px, 'default', 7px);
|
||||
width: tovw(10px, 'default', 8px);
|
||||
height: tovw(10px, 'default', 8px);
|
||||
transition: transform var(--normal-transition);
|
||||
vertical-align: middle;
|
||||
|
||||
&--rotated {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: tovw(1px, 'default', 1px);
|
||||
content: '';
|
||||
animation: border var(--duration-normal) linear infinite;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
background-image: repeating-linear-gradient(
|
||||
0deg,
|
||||
currentcolor,
|
||||
currentcolor 100%,
|
||||
transparent 100%,
|
||||
transparent 100%,
|
||||
currentcolor 100%
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
currentcolor,
|
||||
currentcolor 100%,
|
||||
transparent 100%,
|
||||
transparent 100%,
|
||||
currentcolor 100%
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
currentcolor,
|
||||
currentcolor 100%,
|
||||
transparent 100%,
|
||||
transparent 100%,
|
||||
currentcolor 100%
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
270deg,
|
||||
currentcolor,
|
||||
currentcolor 100%,
|
||||
transparent 100%,
|
||||
transparent 100%,
|
||||
currentcolor 100%
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0, 0 0, 100% 0, 0 100%;
|
||||
background-size: 0 100%, 100% 0, 0 100%, 100% 2px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: tovw(1px);
|
||||
content: '';
|
||||
background: currentcolor;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.icon {
|
||||
transform: rotate(45deg) scale(1.15);
|
||||
}
|
||||
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
background-image: repeating-linear-gradient(
|
||||
0deg,
|
||||
currentcolor,
|
||||
currentcolor tovw(4px, 'default', 3px),
|
||||
transparent tovw(4px, 'default', 3px),
|
||||
transparent tovw(8px, 'default', 6px),
|
||||
currentcolor tovw(8px, 'default', 6px)
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
currentcolor,
|
||||
currentcolor tovw(4px, 'default', 3px),
|
||||
transparent tovw(4px, 'default', 3px),
|
||||
transparent tovw(8px, 'default', 6px),
|
||||
currentcolor tovw(8px, 'default', 6px)
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
currentcolor,
|
||||
currentcolor tovw(4px, 'default', 3px),
|
||||
transparent tovw(4px, 'default', 3px),
|
||||
transparent tovw(8px, 'default', 6px),
|
||||
currentcolor tovw(8px, 'default', 6px)
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
270deg,
|
||||
currentcolor,
|
||||
currentcolor tovw(4px, 'default', 3px),
|
||||
transparent tovw(4px, 'default', 3px),
|
||||
transparent tovw(8px, 'default', 6px),
|
||||
currentcolor tovw(8px, 'default', 6px)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes border {
|
||||
from {
|
||||
background-position: 0 0, tovw(8px, 'default', 6px) 0,
|
||||
100% tovw(8px, 'default', 6px), 0 100%;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 0 tovw(8px, 'default', 6px), 0 0, 100% 0,
|
||||
tovw(8px, 'default', 6px) 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--nav {
|
||||
font-family: var(--font-tt-hoves);
|
||||
font-size: tovw(16px, 'default', 14px);
|
||||
line-height: 1;
|
||||
letter-spacing: tovw(-0.5px, 'default', -0.5px);
|
||||
text-transform: none;
|
||||
|
||||
&::after {
|
||||
content: initial;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 110%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import Link from 'next/link'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import s from './nav-link.module.scss'
|
||||
|
||||
export const NavLink = forwardRef<
|
||||
HTMLAnchorElement,
|
||||
{
|
||||
href: string
|
||||
children?: React.ReactNode
|
||||
title?: string
|
||||
bg?: string
|
||||
}
|
||||
>(({ href, children, title, bg = '#FFE927', ...rest }, ref) => {
|
||||
return (
|
||||
<Link href={href} scroll={false}>
|
||||
<a
|
||||
title={title}
|
||||
className={s.base}
|
||||
style={{ ['--bg' as string]: bg }}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
|
||||
export const NavButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{
|
||||
children?: React.ReactNode
|
||||
title?: string
|
||||
bg?: string
|
||||
onClick?: () => void
|
||||
type?: 'button' | 'submit'
|
||||
}
|
||||
>(({ children, title, bg = '#FFE927', ...rest }, ref) => {
|
||||
return (
|
||||
<button
|
||||
title={title}
|
||||
className={s.base}
|
||||
style={{ ['--bg' as string]: bg }}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
@import '~/css/helpers';
|
||||
|
||||
.base {
|
||||
@apply section-borders;
|
||||
|
||||
--default-border-color: #{theme('colors.green')};
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--default-border-radius);
|
||||
text-transform: uppercase;
|
||||
font-weight: 800;
|
||||
font-size: tovw(18px, 'desktop-large', '12px');
|
||||
padding: 0 1.3333em;
|
||||
height: 2.5555em;
|
||||
line-height: calc(22 / 18);
|
||||
letter-spacing: -0.05em;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
appearance: none;
|
||||
|
||||
@screen tablet {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# `sections` dir
|
||||
|
||||
- Section components for specific sections in the site
|
||||
- Generally nested by page
|
||||
- Examples: `/home/hero.tsx`, `/about/founders.tsx`, `/about/team.tsx`, etc...
|
||||
@@ -0,0 +1,42 @@
|
||||
.section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: calc(var(--vh) * 118 - var(--header-height));
|
||||
text-align: center;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(var(--vh) * 95);
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgb(0 0 0 / 0.5) 45%,
|
||||
hsl(0deg 0% 100% / 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: linear-gradient(0deg, #000 50%, hsl(0deg 0% 100% / 0));
|
||||
}
|
||||
|
||||
.video {
|
||||
position: absolute;
|
||||
z-index: -2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
import Section from '~/components/layout/section'
|
||||
import Heading from '~/components/primitives/heading'
|
||||
import HighlightedText from '~/components/primitives/highlighted-text'
|
||||
|
||||
import s from './hero.module.scss'
|
||||
|
||||
const Hero = () => {
|
||||
return (
|
||||
<Section className={s['section']}>
|
||||
<video
|
||||
autoPlay
|
||||
className={clsx('hide-on-mobile', s['video'])}
|
||||
controls={false}
|
||||
loop
|
||||
muted
|
||||
preload="true"
|
||||
>
|
||||
<source src="/videos/hero-grid.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<Heading as="h1" variant="xl" centered>
|
||||
The <HighlightedText>Multi-chain</HighlightedText> <br /> Verifable Data
|
||||
Marketplace
|
||||
</Heading>
|
||||
<p>
|
||||
From months to minutes. Laconic accelerates <br /> your DApp
|
||||
development, interoperability, <br /> and user adoption.
|
||||
</p>
|
||||
<Heading as="h5" variant="xs" font="dmmono" centered>
|
||||
Scroll Down
|
||||
</Heading>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Hero
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useMedia } from 'hooks/use-media'
|
||||
import * as React from 'react'
|
||||
|
||||
import { DURATION, gsap } from '~/lib/gsap'
|
||||
|
||||
const AnimationContext = React.createContext<
|
||||
| {
|
||||
fontsLoaded: boolean
|
||||
minInteractionTimeComplete: boolean
|
||||
prefersReducedMotion: boolean | undefined
|
||||
shouldAnimate: boolean
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
export const AnimationContextProvider: React.FC = ({ children }) => {
|
||||
const [fontsLoaded, setFontsLoaded] = React.useState(false)
|
||||
const [minInteractionTimeComplete, setMinInteractionTimeComplete] =
|
||||
React.useState(false)
|
||||
const prefersReducedMotion = useMedia('(prefers-reduced-motion: reduce)')
|
||||
|
||||
const shouldAnimate = React.useMemo(
|
||||
() => fontsLoaded && minInteractionTimeComplete && !prefersReducedMotion,
|
||||
[fontsLoaded, minInteractionTimeComplete, prefersReducedMotion]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (window.fontsReady) {
|
||||
setFontsLoaded(true)
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setFontsLoaded(true)
|
||||
}, 500)
|
||||
|
||||
try {
|
||||
document.fonts.ready
|
||||
.then(() => {
|
||||
setFontsLoaded(true)
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(error)
|
||||
setFontsLoaded(true)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
setFontsLoaded(true)
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
setMinInteractionTimeComplete(true)
|
||||
}, DURATION * 1000)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeout)
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (fontsLoaded) {
|
||||
window.fontsReady = true
|
||||
document.documentElement.classList.add('fonts-ready')
|
||||
}
|
||||
}, [fontsLoaded, minInteractionTimeComplete])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (fontsLoaded && minInteractionTimeComplete) {
|
||||
const timeline = gsap.timeline({
|
||||
paused: true,
|
||||
smoothChildTiming: true
|
||||
})
|
||||
|
||||
timeline.to(document.body, {
|
||||
opacity: 1,
|
||||
duration: DURATION,
|
||||
ease: 'BodyFadeIn'
|
||||
})
|
||||
|
||||
timeline.play()
|
||||
|
||||
return () => {
|
||||
timeline.kill()
|
||||
}
|
||||
}
|
||||
}, [fontsLoaded, minInteractionTimeComplete])
|
||||
|
||||
return (
|
||||
<AnimationContext.Provider
|
||||
value={{
|
||||
fontsLoaded,
|
||||
minInteractionTimeComplete,
|
||||
prefersReducedMotion,
|
||||
shouldAnimate
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<style jsx global>{`
|
||||
body {
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
`}</style>
|
||||
</AnimationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useAnimationContext = () => {
|
||||
const context = React.useContext(AnimationContext)
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
'useAnimationContext must be used within a AnimationContextProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
@font-face {
|
||||
font-family: 'Arthemys Display';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
src: url('/fonts/arthemys/ArthemysDisplay-Regular.woff2') format('woff2'),
|
||||
url('/fonts/arthemys/ArthemysDisplay-Regular.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'TT Hoves';
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
src: url('/fonts/tt-hoves/TTHoves-Italic.woff2') format('woff2'),
|
||||
url('/fonts/tt-hoves/TTHoves-Italic.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DM Mono';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
src: url('/fonts/dm-mono/DMMono-Regular.woff2') format('woff2'),
|
||||
url('/fonts/dm-mono/DMMono-Regular.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'TT Hoves';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
src: url('/fonts/tt-hoves/TTHoves-Regular.woff2') format('woff2'),
|
||||
url('/fonts/tt-hoves/TTHoves-Regular.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'TT Hoves';
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
src: url('/fonts/tt-hoves/TTHoves-MediumItalic.woff2') format('woff2'),
|
||||
url('/fonts/tt-hoves/TTHoves-MediumItalic.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'TT Hoves';
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
src: url('/fonts/tt-hoves/TTHoves-Medium.woff2') format('woff2'),
|
||||
url('/fonts/tt-hoves/TTHoves-Medium.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
@import './normalize.css';
|
||||
@import './fonts.css';
|
||||
@import './helpers';
|
||||
@import '../lib/locomotive-scroll/scroll.css';
|
||||
|
||||
:root {
|
||||
--inspect-color: #f00;
|
||||
--font-system: -apple-system, blinkmacsystemfont, segoe ui, roboto, oxygen,
|
||||
ubuntu, cantarell, fira sans, droid sans, helvetica neue, sans-serif;
|
||||
--font-tt-hoves: 'TT Hoves', var(--font-system);
|
||||
--font-arthemys: 'Arthemys Display', var(--font-system);
|
||||
--font-dm-mono: 'DM Mono', var(--font-system);
|
||||
|
||||
// Fonts
|
||||
--font-heading: var(--font-arthemys);
|
||||
|
||||
// Colors
|
||||
--color-accent: #0000f4;
|
||||
--color-black: #040404;
|
||||
--color-white: #fbfbfb;
|
||||
--color-grey-light: #8e8e8e;
|
||||
|
||||
// Duration
|
||||
--duration-normal: 0.525s;
|
||||
--duration-fast: 0.262s;
|
||||
|
||||
// Bezier
|
||||
--ease: cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
|
||||
// Transition
|
||||
--normal-transition: var(--duration-fast) var(--ease);
|
||||
|
||||
/* Main */
|
||||
--main-padding-top: #{tovw(13px, 'default', 13px)};
|
||||
--main-padding-side: #{tovw(57px, 'default', 16px)};
|
||||
|
||||
/* Header */
|
||||
--header-height: #{tovw(71px)};
|
||||
}
|
||||
|
||||
*,
|
||||
*:active,
|
||||
*:focus {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--color-black);
|
||||
text-rendering: geometricprecision;
|
||||
color: var(--color-white);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
-moz-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.15;
|
||||
font-variant-ligatures: common-ligatures;
|
||||
|
||||
&.no-animation {
|
||||
* {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html.has-custom-cursor,
|
||||
html.has-custom-cursor *,
|
||||
html.has-custom-cursor::after,
|
||||
html.has-custom-cursor ::after,
|
||||
html.has-custom-cursor::before,
|
||||
html.has-custom-cursor ::before {
|
||||
cursor: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-black);
|
||||
color: var(--color-white);
|
||||
font-family: var(--font-tthoves);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body:not(.user-is-tabbing) button:focus,
|
||||
body:not(.user-is-tabbing) input:focus,
|
||||
body:not(.user-is-tabbing) select:focus,
|
||||
body:not(.user-is-tabbing) textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: tovw(24px, 'default', 18px);
|
||||
font-family: var(--font-tt-hoves);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.hide-on-mobile {
|
||||
display: inherit !important;
|
||||
@media screen and (max-width: 800px) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-on-desktop {
|
||||
display: initial !important;
|
||||
@media screen and (min-width: 800px) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
@use 'sass:string';
|
||||
@use 'sass:math';
|
||||
|
||||
@function tovw($target, $context: 1920px, $min: 'placeholder') {
|
||||
@if $context == 'default' or $context == 'desktop-large' {
|
||||
$context: 1920px;
|
||||
}
|
||||
|
||||
@if $context == 'desktop' {
|
||||
$context: 1440px;
|
||||
}
|
||||
|
||||
@if $context == 'tablet' {
|
||||
$context: 1024px;
|
||||
}
|
||||
|
||||
@if $context == 'mid-tablet' {
|
||||
$context: 620px;
|
||||
}
|
||||
|
||||
@if $context == 'mobile' {
|
||||
$context: 375px;
|
||||
}
|
||||
|
||||
@if $target == 0 {
|
||||
@return 0;
|
||||
}
|
||||
|
||||
@if $min != 'placeholder' {
|
||||
@return string.unquote(
|
||||
'max(' + $min + ', ' + (math.div($target, $context) * 100) + 'vw)'
|
||||
);
|
||||
}
|
||||
|
||||
@return string.unquote((math.div($target, $context) * 100) + 'vw');
|
||||
}
|
||||
|
||||
@function torem($target, $context: 16px) {
|
||||
@if $target == 0 {
|
||||
@return 0;
|
||||
}
|
||||
|
||||
@return math.div($target, $context) + 0rem;
|
||||
}
|
||||
|
||||
@function toem($target, $context) {
|
||||
@if $target == 0 {
|
||||
@return 0;
|
||||
}
|
||||
|
||||
@return math.div($target, $context) + 0em;
|
||||
}
|
||||
Vendored
+353
@@ -0,0 +1,353 @@
|
||||
/* stylelint-disable font-family-no-duplicate-names */
|
||||
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input {
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type='button']::-moz-focus-inner,
|
||||
[type='reset']::-moz-focus-inner,
|
||||
[type='submit']::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type='button']:-moz-focusring,
|
||||
[type='reset']:-moz-focusring,
|
||||
[type='submit']:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type='checkbox'],
|
||||
[type='radio'] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type='number']::-webkit-inner-spin-button,
|
||||
[type='number']::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type='search']::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as React from 'react'
|
||||
import * as ReactDeviceDetect from 'react-device-detect'
|
||||
|
||||
type DD = {
|
||||
isMobile?: boolean
|
||||
isTablet?: boolean
|
||||
isDesktop?: boolean
|
||||
isMobileSafari?: boolean
|
||||
isMobileOnly?: boolean
|
||||
isSafari?: boolean
|
||||
isChrome?: boolean
|
||||
isFirefox?: boolean
|
||||
isMacOs?: boolean
|
||||
isWindows?: boolean
|
||||
isIOS?: boolean
|
||||
isAndroid?: boolean
|
||||
isBrowser?: boolean
|
||||
}
|
||||
|
||||
export const useDeviceDetect = () => {
|
||||
const [dd, set] = React.useState<DD>({})
|
||||
const [loaded, setLoaded] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
set({
|
||||
isDesktop: ReactDeviceDetect.isDesktop,
|
||||
// window.isMobileOrTablet comes from locomotive-scroll
|
||||
isMobile: window.isMobileOrTablet ?? ReactDeviceDetect.isMobile,
|
||||
isMobileOnly: ReactDeviceDetect.isMobileOnly,
|
||||
isMobileSafari: ReactDeviceDetect.isMobileSafari,
|
||||
isTablet: window.isMobileOrTablet ?? ReactDeviceDetect.isTablet,
|
||||
isChrome: ReactDeviceDetect.isChrome,
|
||||
isFirefox: ReactDeviceDetect.isFirefox,
|
||||
isSafari: ReactDeviceDetect.isSafari,
|
||||
isMacOs: ReactDeviceDetect.isMacOs,
|
||||
isWindows: ReactDeviceDetect.isWindows,
|
||||
isIOS: ReactDeviceDetect.isIOS,
|
||||
isAndroid: ReactDeviceDetect.isAndroid,
|
||||
isBrowser: ReactDeviceDetect.isBrowser
|
||||
})
|
||||
setLoaded(true)
|
||||
}, [])
|
||||
|
||||
return { ...dd, loaded }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { useEffect, useLayoutEffect } from 'react'
|
||||
|
||||
import { isClient } from '~/lib/constants'
|
||||
|
||||
export const useIsomorphicLayoutEffect = isClient ? useLayoutEffect : useEffect
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ResizeObserver } from '@juggle/resize-observer'
|
||||
import _useMeasure, { Options } from 'react-use-measure'
|
||||
|
||||
export const useMeasure = (opts?: Options) => {
|
||||
return _useMeasure({ polyfill: ResizeObserver, ...opts })
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { isApiSupported } from '~/lib/utils'
|
||||
|
||||
export const useMedia = (mediaQuery: string, initialValue?: boolean) => {
|
||||
const [isVerified, setIsVerified] = React.useState<boolean | undefined>(
|
||||
initialValue
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isApiSupported('matchMedia')) {
|
||||
console.warn('matchMedia is not supported by your current browser')
|
||||
return
|
||||
}
|
||||
const mediaQueryList = window.matchMedia(mediaQuery)
|
||||
const changeHandler = () => setIsVerified(!!mediaQueryList.matches)
|
||||
|
||||
changeHandler()
|
||||
if (typeof mediaQueryList.addEventListener === 'function') {
|
||||
mediaQueryList.addEventListener('change', changeHandler)
|
||||
return () => {
|
||||
mediaQueryList.removeEventListener('change', changeHandler)
|
||||
}
|
||||
} else if (typeof mediaQueryList.addListener === 'function') {
|
||||
mediaQueryList.addListener(changeHandler)
|
||||
return () => {
|
||||
mediaQueryList.removeListener(changeHandler)
|
||||
}
|
||||
}
|
||||
}, [mediaQuery])
|
||||
|
||||
return isVerified
|
||||
}
|
||||
|
||||
export const useIsMobile = () => {
|
||||
return useMedia('(max-width: 767px)')
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as React from 'react'
|
||||
|
||||
export const useReactId = () => {
|
||||
return (React as any).useId() as string
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { EventHandler } from 'locomotive-scroll'
|
||||
import * as React from 'react'
|
||||
|
||||
import { useLocomotiveScroll } from '~/lib/locomotive-scroll/provider'
|
||||
|
||||
export type ScrollListenerHandlers = {
|
||||
smoothHandler: EventHandler
|
||||
nativeHandler: (e: Event) => void
|
||||
}
|
||||
|
||||
export const useScrollListener = ({
|
||||
smoothHandler,
|
||||
nativeHandler
|
||||
}: ScrollListenerHandlers) => {
|
||||
const { scroll, isSmooth } = useLocomotiveScroll()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!scroll) return
|
||||
if (isSmooth) {
|
||||
const handler: EventHandler = (args) => {
|
||||
smoothHandler(args)
|
||||
}
|
||||
|
||||
scroll.on('scroll', handler)
|
||||
return () => {
|
||||
scroll.off('scroll', handler)
|
||||
}
|
||||
} else {
|
||||
const handler = (event: Event) => {
|
||||
nativeHandler(event)
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handler)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handler)
|
||||
}
|
||||
}
|
||||
}, [isSmooth, nativeHandler, scroll, smoothHandler])
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { gsap } from '~/lib/gsap'
|
||||
import { useLocomotiveScroll } from '~/lib/locomotive-scroll/provider'
|
||||
|
||||
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect'
|
||||
|
||||
/**
|
||||
* With a similar API as `React.useEffect`, this hook will create a `gsap.timeline` and run a callback with arbitrary dependencies.
|
||||
*
|
||||
* ```tsx
|
||||
* useTimelineEffect((tl) => {
|
||||
* tl.to(".my-element", { duration: 1, opacity: 0 });
|
||||
* tl.to(".my-other-element", { duration: 1, opacity: 0 });
|
||||
*
|
||||
* return () => {
|
||||
* // cleanup
|
||||
* tl.kill();
|
||||
* }
|
||||
* }, []);
|
||||
* ```
|
||||
*
|
||||
* @param callback A callback that is executed whenever dependencies change. Receives the timeline as a parameter. Returns a cleanup function.
|
||||
* @param dependencies Dependencies for the effect.
|
||||
* @param options Common options.
|
||||
* @returns
|
||||
*/
|
||||
export const useTimelineEffect = (
|
||||
callback: (timeline: GSAPTimeline) => void | (() => void),
|
||||
dependencies: React.DependencyList,
|
||||
options?: { autoKill?: boolean; autoPlay?: boolean }
|
||||
) => {
|
||||
const { isReady } = useLocomotiveScroll()
|
||||
const [timeline] = React.useState(() =>
|
||||
gsap.timeline({ paused: options?.autoPlay ? false : true })
|
||||
)
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (!isReady) return
|
||||
const cleanup = callback?.(timeline)
|
||||
return () => {
|
||||
cleanup?.()
|
||||
if (options?.autoKill !== false) {
|
||||
timeline.kill()
|
||||
}
|
||||
}
|
||||
}, [isReady, callback, timeline, options, ...(dependencies || [])])
|
||||
|
||||
return { timeline }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react'
|
||||
|
||||
export const useToggleState = (initialState = false) => {
|
||||
const [isOn, setIsOn] = React.useState(initialState)
|
||||
|
||||
const handleOn = React.useCallback(() => {
|
||||
setIsOn(true)
|
||||
}, [])
|
||||
|
||||
const handleOff = React.useCallback(() => {
|
||||
setIsOn(false)
|
||||
}, [])
|
||||
|
||||
const handleToggle = React.useCallback(() => {
|
||||
setIsOn((p) => !p)
|
||||
}, [])
|
||||
|
||||
return { isOn, handleToggle, handleOn, handleOff }
|
||||
}
|
||||
|
||||
export type ToggleState = ReturnType<typeof useToggleState>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextApiResponse } from 'next'
|
||||
|
||||
import { formatError } from './utils'
|
||||
|
||||
// Some helpers for usual http responses
|
||||
|
||||
export function success(
|
||||
res: NextApiResponse,
|
||||
json: { [key: string]: unknown } = {}
|
||||
) {
|
||||
return res.status(200).json(json)
|
||||
}
|
||||
|
||||
export function badRequest(
|
||||
res: NextApiResponse,
|
||||
error: unknown = 'Bad Request'
|
||||
) {
|
||||
console.error(error)
|
||||
return res.status(400).json({ error: formatError(error) })
|
||||
}
|
||||
|
||||
export function notAuthorized(
|
||||
res: NextApiResponse,
|
||||
error: unknown = 'Not Authorized'
|
||||
) {
|
||||
console.error(error)
|
||||
return res.status(401).json({ error: formatError(error) })
|
||||
}
|
||||
|
||||
export function notFound(res: NextApiResponse, error: unknown = 'Not Found') {
|
||||
console.error(error)
|
||||
return res.status(404).json({ error: formatError(error) })
|
||||
}
|
||||
|
||||
export function internalServerError(
|
||||
res: NextApiResponse,
|
||||
error: unknown,
|
||||
code = 500
|
||||
) {
|
||||
console.error(error)
|
||||
return res.status(code).json({ error: formatError(error) })
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
export const isDev = process.env.NODE_ENV === 'development'
|
||||
export const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
export const isClient = typeof window !== 'undefined'
|
||||
export const isServer = !isClient
|
||||
|
||||
export const siteURL = new URL(
|
||||
process.env.NEXT_PUBLIC_SITE_URL ??
|
||||
(isDev ? 'http://localhost:3000' : 'https://www.laconic.com/') // TODO: use your actual production url as default
|
||||
)
|
||||
export const siteOrigin = siteURL.origin
|
||||
|
||||
// this is not used anywhere — just for our (basement.) projects.
|
||||
// you can delete it if not needed.
|
||||
export const basementLog = `
|
||||
|
||||
██╗
|
||||
██║
|
||||
██████╗
|
||||
██╔══██╗ ██╗
|
||||
██████╔╝ ██╝
|
||||
╚═════╝
|
||||
|
||||
From the basement. https://basement.studio
|
||||
`
|
||||
|
||||
export const defaultMeta = {
|
||||
title: 'Laconic Network',
|
||||
description: ``,
|
||||
ogImage: `${siteOrigin}/og.jpeg`,
|
||||
twitter: {
|
||||
handle: '@laconicnetwork',
|
||||
site: '@laconicnetwork'
|
||||
}
|
||||
}
|
||||
|
||||
export const gaTrackingId = 'G-9VWWSHF995'
|
||||
|
||||
export const socialLinks = {
|
||||
youTube: 'https://www.youtube.com/c/MrBeast6000',
|
||||
twitter: 'https://twitter.com/laconicnetwork',
|
||||
instagram: 'https://www.instagram.com/mrbeast/',
|
||||
tikTok: 'https://www.tiktok.com/@mrbeast',
|
||||
discord: 'https://discord.com/invite/ukhbBemyxY',
|
||||
telegram: 'https://t.me/laconicnetwork',
|
||||
email: 'mailto:support@shopmrbeast.com',
|
||||
sms: 'sms:+1(877)740-1782'
|
||||
}
|
||||
|
||||
export const mode = (process.env.NEXT_PUBLIC_MODE ?? 'default') as
|
||||
| 'default'
|
||||
| 'showcase'
|
||||
@@ -0,0 +1,35 @@
|
||||
import Script from 'next/script'
|
||||
|
||||
import { isClient } from '../constants'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
fontsReady: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const encodeBase64 = (str: string) => {
|
||||
return isClient ? window.btoa(str) : Buffer.from(str).toString('base64')
|
||||
}
|
||||
|
||||
export const FontsReadyScript = () => {
|
||||
const encodedScript = `data:text/javascript;base64,${encodeBase64(`
|
||||
function onReady() {
|
||||
window.fontsReady = true
|
||||
document.documentElement.classList.add('fonts-ready')
|
||||
}
|
||||
try {
|
||||
document.fonts.ready
|
||||
.then(() => {
|
||||
onReady()
|
||||
})
|
||||
.catch(() => {
|
||||
onReady()
|
||||
})
|
||||
} catch (error) {
|
||||
onReady()
|
||||
}
|
||||
`)}`
|
||||
|
||||
return <Script strategy="beforeInteractive" src={encodedScript} />
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import Script from 'next/script'
|
||||
import * as React from 'react'
|
||||
|
||||
import { gaTrackingId } from './constants'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
gtag: any
|
||||
}
|
||||
}
|
||||
|
||||
// https://developers.google.com/analytics/devguides/collection/gtagjs/pages
|
||||
export const pageview = (url: string) => {
|
||||
window.gtag('config', gaTrackingId, {
|
||||
page_path: url
|
||||
})
|
||||
}
|
||||
|
||||
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
|
||||
export const event = ({
|
||||
action,
|
||||
category,
|
||||
label,
|
||||
value
|
||||
}: {
|
||||
action: string
|
||||
category: string
|
||||
label: string
|
||||
value: string
|
||||
}) => {
|
||||
window.gtag('event', action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
value: value
|
||||
})
|
||||
}
|
||||
|
||||
export const GAScripts = () => {
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
strategy="afterInteractive"
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${gaTrackingId}`}
|
||||
/>
|
||||
<Script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '${gaTrackingId}');
|
||||
gtag('config', 'AW-765997558');
|
||||
`
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Use this hook in _app.tsx
|
||||
export const useAppGA = () => {
|
||||
const router = useRouter()
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleRouteChange = (url: string) => {
|
||||
pageview(url)
|
||||
}
|
||||
router.events.on('routeChangeComplete', handleRouteChange)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', handleRouteChange)
|
||||
}
|
||||
}, [router.events])
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import gsap from 'gsap'
|
||||
import { CSSRulePlugin } from 'gsap/dist/CSSRulePlugin'
|
||||
import { CustomEase } from 'gsap/dist/CustomEase'
|
||||
import { SplitText } from 'gsap/dist/SplitText'
|
||||
|
||||
gsap.registerPlugin(CSSRulePlugin, CustomEase, SplitText)
|
||||
|
||||
const GOLDEN_RATIO = (1 + Math.sqrt(5)) / 2
|
||||
const RECIPROCAL_GR = 1 / GOLDEN_RATIO
|
||||
const DURATION = RECIPROCAL_GR * 0.85
|
||||
const CUSTOM_EASE = CustomEase.create('EaseIn', '0.165, 0.84, 0.44, 1')
|
||||
CustomEase.create('BodyFadeIn', 'M0,0 C0.056,0.208 0.6,0.898 1,1')
|
||||
|
||||
export type RegisteredEffects =
|
||||
| 'fadeIn'
|
||||
| 'fadeInBottom'
|
||||
| 'fadeInScale'
|
||||
| 'in'
|
||||
| 'out'
|
||||
|
||||
gsap.config({
|
||||
autoSleep: 60,
|
||||
nullTargetWarn: false
|
||||
})
|
||||
|
||||
gsap.defaults({
|
||||
ease: 'EaseIn',
|
||||
duration: DURATION
|
||||
})
|
||||
|
||||
gsap.registerEffect({
|
||||
name: 'fadeIn',
|
||||
extendTimeline: true,
|
||||
defaults: {
|
||||
delay: 0,
|
||||
duration: DURATION,
|
||||
scale: 1,
|
||||
stagger: DURATION / 8,
|
||||
y: 30
|
||||
},
|
||||
effect: (targets: Array<gsap.TweenTarget>, config: gsap.TweenVars) => {
|
||||
const tl = gsap.timeline()
|
||||
tl.from(targets, {
|
||||
autoAlpha: 0,
|
||||
delay: config.delay,
|
||||
duration: config.duration,
|
||||
ease: config.ease,
|
||||
stagger: config.stagger,
|
||||
scale: config.scale,
|
||||
y: config.y
|
||||
})
|
||||
return tl
|
||||
}
|
||||
})
|
||||
|
||||
gsap.registerEffect({
|
||||
name: 'fadeInScale',
|
||||
extendTimeline: true,
|
||||
defaults: {
|
||||
delay: 0,
|
||||
duration: DURATION,
|
||||
scale: 0.4,
|
||||
stagger: DURATION / 6,
|
||||
y: 30
|
||||
},
|
||||
effect: (targets: Array<gsap.TweenTarget>, config: gsap.TweenVars) => {
|
||||
const tl = gsap.timeline()
|
||||
tl.from(targets, {
|
||||
autoAlpha: 0,
|
||||
delay: config.delay,
|
||||
duration: config.duration,
|
||||
ease: config.ease,
|
||||
stagger: config.stagger,
|
||||
scale: config.scale,
|
||||
y: config.y
|
||||
})
|
||||
return tl
|
||||
}
|
||||
})
|
||||
|
||||
gsap.registerEffect({
|
||||
name: 'in',
|
||||
extendTimeline: true,
|
||||
defaults: {
|
||||
duration: DURATION,
|
||||
each: DURATION / 20,
|
||||
ease: 'power3.inOut',
|
||||
fade: DURATION,
|
||||
from: 'start',
|
||||
perspective: 500,
|
||||
rotationX: 2,
|
||||
rotationY: 20,
|
||||
scale: 0.96,
|
||||
staggerEase: 'EaseIn',
|
||||
transformOrigin: '0% 100%',
|
||||
x: 0,
|
||||
y: 0,
|
||||
xPercent: 23,
|
||||
yPercent: 0
|
||||
},
|
||||
effect: (targets: Array<HTMLElement>, config: gsap.TweenVars) => {
|
||||
if (
|
||||
config.rotationX !== 0 ||
|
||||
config.rotationY !== 0 ||
|
||||
!targets ||
|
||||
targets.length > 0
|
||||
) {
|
||||
gsap.set(targets[0]?.parentNode, { perspective: config.perspective })
|
||||
}
|
||||
if (config.yPercent !== 0) {
|
||||
gsap.set(targets[0]?.parentNode, { overflow: 'hidden' })
|
||||
}
|
||||
|
||||
const tl = gsap.timeline()
|
||||
tl.from(targets, {
|
||||
duration: config.duration,
|
||||
ease: config.ease,
|
||||
rotationX: config.rotationX,
|
||||
rotationY: config.rotationY,
|
||||
scale: config.scale,
|
||||
transformOrigin: config.transformOrigin,
|
||||
x: config.x,
|
||||
y: config.y,
|
||||
yPercent: config.yPercent,
|
||||
xPercent: config.xPercent,
|
||||
stagger: {
|
||||
each: config.each,
|
||||
ease: config.staggerEase,
|
||||
from: config.from
|
||||
}
|
||||
})
|
||||
|
||||
tl.from(
|
||||
targets,
|
||||
{
|
||||
duration: config.fade,
|
||||
ease: 'none',
|
||||
opacity: 0,
|
||||
stagger: {
|
||||
each: config.each,
|
||||
ease: config.staggerEase,
|
||||
from: config.from
|
||||
}
|
||||
},
|
||||
0
|
||||
)
|
||||
return tl
|
||||
}
|
||||
})
|
||||
|
||||
export { CSSRulePlugin, CUSTOM_EASE, DURATION, gsap, SplitText }
|
||||
@@ -0,0 +1,147 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { useIsomorphicLayoutEffect } from '~/hooks/use-isomorphic-layout-effect'
|
||||
|
||||
type TransitionCallback = () => Promise<void>
|
||||
type TransitionOptions = { index?: number; kill?: boolean }
|
||||
|
||||
type GetTransitionSpace = (
|
||||
callback: TransitionCallback,
|
||||
options?: TransitionOptions
|
||||
) => void
|
||||
const TransitionContext = React.createContext<
|
||||
| {
|
||||
transitionsListRef: React.MutableRefObject<
|
||||
Array<{
|
||||
callback: TransitionCallback
|
||||
options?: TransitionOptions
|
||||
}>
|
||||
>
|
||||
getTransitionSpace: GetTransitionSpace
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
const TransitionContextProvider = ({
|
||||
children
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
}) => {
|
||||
const transitionsListRef = React.useRef<
|
||||
Array<{ callback: TransitionCallback; options?: TransitionOptions }>
|
||||
>([])
|
||||
|
||||
const getTransitionSpace: GetTransitionSpace = React.useCallback(
|
||||
(callback: TransitionCallback, options) => {
|
||||
if (options?.index) {
|
||||
transitionsListRef.current.splice(options.index, 0, {
|
||||
callback,
|
||||
options
|
||||
})
|
||||
} else {
|
||||
transitionsListRef.current.push({ callback, options })
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
Array.from(
|
||||
document.querySelectorAll('head > link[rel="stylesheet"][data-n-p]')
|
||||
).forEach((node) => {
|
||||
node.removeAttribute('data-n-p')
|
||||
})
|
||||
const mutationHandler: MutationCallback = (mutations) => {
|
||||
mutations.forEach(({ target }) => {
|
||||
if (target.nodeName === 'STYLE') {
|
||||
const style = target as HTMLStyleElement
|
||||
if (style.getAttribute('media') === 'x') {
|
||||
style.removeAttribute('media')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
const observer = new MutationObserver(mutationHandler)
|
||||
observer.observe(document.head, {
|
||||
subtree: true,
|
||||
attributeFilter: ['media']
|
||||
})
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<TransitionContext.Provider
|
||||
value={{ transitionsListRef, getTransitionSpace }}
|
||||
>
|
||||
{children}
|
||||
</TransitionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const usePageTransition = () => {
|
||||
const ctx = React.useContext(TransitionContext)
|
||||
if (ctx === undefined) {
|
||||
throw new Error(
|
||||
'usePageTransition must be used within a PageTransitionsProvider'
|
||||
)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// This is another component so that it doesn't trigger a re-render in the context provider
|
||||
const TransitionLayout = React.memo(
|
||||
({ children }: { children?: React.ReactNode }) => {
|
||||
const [displayChildren, setDisplayChildren] = React.useState(children)
|
||||
const { transitionsListRef } = usePageTransition()
|
||||
const oldPathnameRef = React.useRef<string>('')
|
||||
|
||||
React.useEffect(() => {
|
||||
// init pathname
|
||||
oldPathnameRef.current = window.location.pathname
|
||||
}, [])
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const newPathname = window.location.pathname
|
||||
if (
|
||||
children !== displayChildren &&
|
||||
oldPathnameRef.current !== newPathname
|
||||
) {
|
||||
if (transitionsListRef.current.length === 0) {
|
||||
// there are no outro animations, so immediately transition
|
||||
setDisplayChildren(children)
|
||||
oldPathnameRef.current = newPathname
|
||||
} else {
|
||||
const transitionsPromise = transitionsListRef.current.map(
|
||||
async (transition) => {
|
||||
await transition.callback()
|
||||
return transition
|
||||
}
|
||||
)
|
||||
Promise.all(transitionsPromise).then((resolvedTransitions) => {
|
||||
setDisplayChildren(children)
|
||||
oldPathnameRef.current = newPathname
|
||||
transitionsListRef.current = resolvedTransitions.filter((t) =>
|
||||
t.options?.kill ? false : true
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [children, transitionsListRef])
|
||||
|
||||
return <>{displayChildren}</>
|
||||
}
|
||||
)
|
||||
|
||||
export const PageTransitionsProvider = ({
|
||||
children
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<TransitionContextProvider>
|
||||
<TransitionLayout>{children}</TransitionLayout>
|
||||
</TransitionContextProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { Scroll } from 'locomotive-scroll'
|
||||
import * as React from 'react'
|
||||
import mergeRefs from 'react-merge-refs'
|
||||
|
||||
import { useMeasure } from '~/hooks/use-measure'
|
||||
|
||||
import { LocomotiveScrollScripts } from './scripts'
|
||||
|
||||
// Scroll lerp value
|
||||
export const lerpScroll = 0.09708
|
||||
|
||||
export interface Context {
|
||||
/**
|
||||
* LocomotiveScroll instance
|
||||
*/
|
||||
scroll: Scroll | null
|
||||
/**
|
||||
* If LocomotiveScroll is mounted
|
||||
*/
|
||||
isReady: boolean
|
||||
/**
|
||||
* If isMobile, isSmooth will be false and native behaviour will kick in.
|
||||
*/
|
||||
isSmooth: boolean | undefined
|
||||
}
|
||||
|
||||
const LocomotiveScrollContext = React.createContext<Context | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const LocomotiveScrollProvider = ({ children }: Props) => {
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
const locomotiveScrollRef = React.useRef<Scroll | null>(null)
|
||||
const scrollContainerRef = React.useRef<HTMLDivElement>(null)
|
||||
const [ref, { height, width }] = useMeasure({ debounce: 100 })
|
||||
const [isSmooth, setIsSmooth] = React.useState<boolean>()
|
||||
|
||||
React.useEffect(() => {
|
||||
;(async () => {
|
||||
try {
|
||||
const isMobileOrTablet = window.isMobileOrTablet
|
||||
|
||||
if (isMobileOrTablet) {
|
||||
setIsSmooth(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (!scrollContainerRef.current) {
|
||||
setIsSmooth(false)
|
||||
return
|
||||
}
|
||||
|
||||
const LocomotiveScroll = (await import('locomotive-scroll')).default
|
||||
|
||||
const locoScroll = new LocomotiveScroll({
|
||||
el: scrollContainerRef.current,
|
||||
smooth: true,
|
||||
lerp: lerpScroll,
|
||||
firefoxMultiplier: 100
|
||||
})
|
||||
|
||||
locomotiveScrollRef.current = locoScroll
|
||||
setIsSmooth(true)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
setIsReady(true) // Re-render the context
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
locomotiveScrollRef.current?.destroy()
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
locomotiveScrollRef.current?.update()
|
||||
}, [height, width])
|
||||
|
||||
return (
|
||||
<LocomotiveScrollContext.Provider
|
||||
value={{ scroll: locomotiveScrollRef.current, isSmooth, isReady }}
|
||||
>
|
||||
<div
|
||||
ref={mergeRefs([scrollContainerRef, ref])}
|
||||
className="bg-black-200 scroll-container"
|
||||
data-scroll-container
|
||||
>
|
||||
<LocomotiveScrollScripts />
|
||||
{children}
|
||||
</div>
|
||||
</LocomotiveScrollContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useLocomotiveScroll = () => {
|
||||
const ctx = React.useContext(LocomotiveScrollContext)
|
||||
if (ctx === undefined) {
|
||||
throw new Error('useLocomotiveScroll: Context not found')
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
import Script from 'next/script'
|
||||
import * as React from 'react'
|
||||
|
||||
export const tabletBreakpoint = 1024
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
isMobileOrTablet: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const LocomotiveScrollScripts = React.memo(() => (
|
||||
<>
|
||||
<Script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
function checkIfMobileOrTablet() {
|
||||
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) || window.innerWidth < ${tabletBreakpoint};
|
||||
const isTablet = isMobile && window.innerWidth >= ${tabletBreakpoint};
|
||||
const testSubject = navigator.userAgent||navigator.vendor||window.opera
|
||||
const reg1 = new RegExp("(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk")
|
||||
const reg2 = new RegExp("1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-")
|
||||
return isMobile || isTablet || reg1.test(testSubject) || reg2.test(testSubject.substr(0,4))
|
||||
};
|
||||
|
||||
window.isMobileOrTablet = checkIfMobileOrTablet();
|
||||
|
||||
if(!window.isMobileOrTablet) {
|
||||
document.documentElement.classList.add('has-scroll-smooth')
|
||||
}
|
||||
`
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
))
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as React from 'react'
|
||||
|
||||
export const ScrollSectionBox = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
JSX.IntrinsicElements['div'] & { disabled?: boolean }
|
||||
>(({ children, disabled, ...rest }, ref) => {
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
{...(!disabled ? { 'data-scroll-section': true } : {})}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
Vendored
+228
@@ -0,0 +1,228 @@
|
||||
declare module 'locomotive-scroll' {
|
||||
export function getParents(elem: Element): Element[]
|
||||
export function queryClosestParent(
|
||||
elem: Element,
|
||||
selector: string
|
||||
): Element | null
|
||||
export function transform(el: Element, transformValue: string): void
|
||||
export function getTranslate(el: Element): Vector2
|
||||
|
||||
export type Vector2 = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface ScrollToOptions {
|
||||
/**
|
||||
* Defines an offset from your target. E.g. -100 if you want to scroll 100 pixels above your target.
|
||||
*/
|
||||
offset?: number | string
|
||||
/**
|
||||
* Defines the duration of the scroll animation in milliseconds. Defaults to 1000.
|
||||
*/
|
||||
duration?: number
|
||||
/**
|
||||
* An array of 4 floats between 0 and 1 defining the bezier curve for the animation's easing.
|
||||
*
|
||||
* Defaults to `[0.25, 0.00, 0.35, 1.00]`
|
||||
*
|
||||
* See http://greweb.me/bezier-easing-editor/example/
|
||||
*
|
||||
* Keep in mind this will also be affected by the lerp unless you set `disableLerp` to `true`.
|
||||
*/
|
||||
easing?: [number, number, number, number]
|
||||
/**
|
||||
* Lerp effect won't be applied if set to true.
|
||||
*/
|
||||
disableLerp?: boolean
|
||||
/**
|
||||
* Called when scrollTo completes (note that it won't wait for lerp to stabilize).
|
||||
*/
|
||||
callback?: () => void
|
||||
}
|
||||
|
||||
export interface LocomotiveScrollOptions {
|
||||
/** Scroll container element. */
|
||||
el?: Element
|
||||
/** Data attribute prefix (data-scroll-xxxx). */
|
||||
name?: string
|
||||
elMobile?: Element
|
||||
/**
|
||||
* Global in-view trigger offset : [bottom,top]
|
||||
* Use a string with % to use a percentage of the viewport height.
|
||||
* Use a numeric value for absolute pixels unit.
|
||||
*/
|
||||
offset?: [number, number]
|
||||
/**
|
||||
* Repeat in-view detection.
|
||||
*/
|
||||
repeat?: boolean
|
||||
/**
|
||||
* Smooth scrolling.
|
||||
*/
|
||||
smooth?: boolean
|
||||
/**
|
||||
* An object defining the initial scroll coordinates on a smooth instance. For example: { x: 0, y: 1000 }
|
||||
*/
|
||||
initPosition?: { x: number; y: number }
|
||||
/**
|
||||
* Scroll direction: vertical or horizontal.
|
||||
*/
|
||||
direction?: 'vertical' | 'horizontal'
|
||||
/**
|
||||
* Linear interpolation (lerp) intensity. Float between 0 and 1.
|
||||
* This defines the "smoothness" intensity. The closer to 0, the smoother.
|
||||
*/
|
||||
lerp?: number
|
||||
/**
|
||||
* Add direction to scroll event.
|
||||
*/
|
||||
getDirection?: boolean
|
||||
/**
|
||||
* Add speed to scroll event.
|
||||
*/
|
||||
getSpeed?: boolean
|
||||
/**
|
||||
* Element in-view class.
|
||||
*/
|
||||
class?: string
|
||||
/**
|
||||
* Initialize class.
|
||||
*/
|
||||
initClass?: string
|
||||
/**
|
||||
* Is scrolling class.
|
||||
*/
|
||||
scrollingClass?: string
|
||||
/**
|
||||
* Is dragging class.
|
||||
*/
|
||||
draggingClass?: string
|
||||
/**
|
||||
* Has smooth scrolling class.
|
||||
*/
|
||||
smoothClass?: string
|
||||
/**
|
||||
* Scrollbar element class.
|
||||
*/
|
||||
scrollbarClass?: string
|
||||
/**
|
||||
* Specifies the container element for the scrollbar to be appended in. If false, scrollbar will be appended to the body.
|
||||
*/
|
||||
scrollbarContainer?: Element | false
|
||||
/**
|
||||
* Factor applied to the scroll delta, allowing to boost/reduce scrolling speed (regardless of the platform).
|
||||
*/
|
||||
multiplier?: number
|
||||
/**
|
||||
* Boost scrolling speed of Firefox on Windows.
|
||||
*/
|
||||
firefoxMultiplier?: number
|
||||
/**
|
||||
* Multiply touch action to scroll faster than finger movement.
|
||||
*/
|
||||
touchMultiplier?: number
|
||||
/**
|
||||
* By default locomotive-scroll listens for scroll events only on the scroll container (`el` option). With this option set to true, it listens on the whole document instead.
|
||||
*/
|
||||
scrollFromAnywhere?: boolean
|
||||
/**
|
||||
* Defines which gesture direction(s) scrolls in your instance. You can use:
|
||||
* - `vertical`
|
||||
* - `horizontal`
|
||||
* - `both`
|
||||
*/
|
||||
gestureDirection?: 'vertical' | 'horizontal' | 'both'
|
||||
/**
|
||||
* Object allowing to override some options for a particular context. You can specify:
|
||||
* - `smooth`
|
||||
* - `direction`
|
||||
* - `horizontalGesture`
|
||||
*
|
||||
* For tablet context you can also define breakpoint (integer, defaults to 1024) to set the max-width breakpoint for tablets.
|
||||
*/
|
||||
tablet?: {
|
||||
smooth?: boolean
|
||||
direction?: 'vertical' | 'horizontal'
|
||||
horizontalGesture?: boolean
|
||||
breakpoint?: number
|
||||
}
|
||||
/**
|
||||
* Object allowing to override some options for a particular context. You can specify:
|
||||
* - `smooth`
|
||||
* - `direction`
|
||||
* - `horizontalGesture`
|
||||
*
|
||||
* For tablet context you can also define breakpoint (integer, defaults to 1024) to set the max-width breakpoint for tablets.
|
||||
*/
|
||||
smartphone?: {
|
||||
smooth?: boolean
|
||||
direction?: 'vertical' | 'horizontal'
|
||||
horizontalGesture?: boolean
|
||||
}
|
||||
/**
|
||||
* Allows to reload the page when switching between `desktop`, `tablet` and `smartphone` contexts. It can be useful if your page changes a lot between contexts and you want to reset everything.
|
||||
*/
|
||||
reloadOnContextChange?: boolean
|
||||
/**
|
||||
* Sets `history.scrollRestoration = 'manual'` and calls `window.scrollTo(0, 0)` on locomotive-scroll init in Native Class. Useful if you use transitions with native scrolling, otherwise we advise to set it to `false` if you don't want to break History API's scroll restoration feature.
|
||||
*/
|
||||
resetNativeScroll?: boolean
|
||||
}
|
||||
|
||||
export type EventHandler = (data: {
|
||||
currentElements: Record<string, unknown>
|
||||
delta: { x: number; y: number }
|
||||
limit: { x: number; y: number }
|
||||
scroll: { x: number; y: number }
|
||||
speed: number
|
||||
}) => void
|
||||
|
||||
export default class LocomotiveScroll implements LocomotiveScrollOptions {
|
||||
constructor(options?: LocomotiveScrollOptions)
|
||||
|
||||
/**
|
||||
* Reinitializes the scroll.
|
||||
*/
|
||||
init(): void
|
||||
/**
|
||||
* Scroller element.
|
||||
*/
|
||||
el: HTMLElement
|
||||
/**
|
||||
* Updates all element positions.
|
||||
*/
|
||||
update(): void
|
||||
/**
|
||||
* Destroys the scroll events.
|
||||
*/
|
||||
destroy(): void
|
||||
/**
|
||||
* Restarts the scroll events.
|
||||
*/
|
||||
start(): void
|
||||
/**
|
||||
* Stops the scroll events.
|
||||
*/
|
||||
stop(): void
|
||||
/**
|
||||
* Scroll to a target.
|
||||
* @param target Defines where you want to scroll.
|
||||
* @param options Settings object.
|
||||
*/
|
||||
scrollTo(
|
||||
target: Node | string | 'top' | 'bottom' | number,
|
||||
options?: ScrollToOptions
|
||||
): void
|
||||
/**
|
||||
* todo, type this
|
||||
*/
|
||||
on(event: 'scroll', handler: EventHandler): void
|
||||
/**
|
||||
* todo, type this
|
||||
*/
|
||||
off(event: 'scroll', handler: EventHandler): void
|
||||
}
|
||||
|
||||
export { LocomotiveScroll as Scroll }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
type ParallaxPresets = 'up' | 'down'
|
||||
|
||||
export function parallax(speed: number | ParallaxPresets) {
|
||||
let speedValue = speed
|
||||
switch (speed) {
|
||||
case 'up':
|
||||
speedValue = 0.6
|
||||
break
|
||||
case 'down':
|
||||
speedValue = -0.6
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return { 'data-scroll': true, 'data-scroll-speed': speedValue }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { isClient } from '~/lib/constants'
|
||||
|
||||
export const formatError = (
|
||||
error: unknown
|
||||
): { message: string; name?: string } => {
|
||||
try {
|
||||
if (error instanceof Error) {
|
||||
return { message: error.message, name: error.name }
|
||||
}
|
||||
return { message: String(error) }
|
||||
} catch (error) {
|
||||
return { message: 'An unknown error ocurred.' }
|
||||
}
|
||||
}
|
||||
|
||||
export const isApiSupported = (api: string) => isClient && api in window
|
||||
|
||||
export function lerp(v0: number, v1: number, t: number) {
|
||||
return v0 * (1 - t) + v1 * t
|
||||
}
|
||||
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2
|
||||
})
|
||||
|
||||
export const formatPrice = (amount: string) => {
|
||||
return formatter.format(parseFloat(amount))
|
||||
}
|
||||
|
||||
const COUNT_ABBRS = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
|
||||
|
||||
export const formatStatCount = (count = 0, withAbbr = false, decimals = 2) => {
|
||||
const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000))
|
||||
const result = parseFloat((count / Math.pow(1000, i)).toFixed(decimals))
|
||||
if (withAbbr) {
|
||||
return `${result + COUNT_ABBRS[i]}`
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { NextRouter } from 'next/dist/client/router'
|
||||
|
||||
import { siteOrigin } from '~/lib/constants'
|
||||
|
||||
export type QueryParams = { [key: string]: string | null }
|
||||
|
||||
export const cleanPath = (asPath: string) => {
|
||||
const uri = new URL(asPath, siteOrigin)
|
||||
return uri.pathname
|
||||
}
|
||||
|
||||
export const checkIsExternal = (href: string) => {
|
||||
try {
|
||||
const url = new URL(href)
|
||||
const { hostname } = new URL(siteOrigin)
|
||||
if (url.hostname !== hostname) return true
|
||||
} catch (error) {
|
||||
// failed cause href is relative
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const getHrefWithQuery = (
|
||||
asPath: string,
|
||||
newQueryParams?: QueryParams,
|
||||
override = true
|
||||
) => {
|
||||
const uri = new URL(asPath, siteOrigin)
|
||||
|
||||
if (newQueryParams) {
|
||||
Object.keys(newQueryParams).forEach((key) => {
|
||||
const value = newQueryParams[key]
|
||||
if (value === null) {
|
||||
if (override) uri.searchParams.delete(key)
|
||||
return
|
||||
}
|
||||
if (uri.searchParams.has(key) && override) {
|
||||
uri.searchParams.delete(key)
|
||||
}
|
||||
uri.searchParams.append(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
return `${uri.pathname}${uri.search}${uri.hash}`
|
||||
}
|
||||
|
||||
export type TransitionOptions = Parameters<NextRouter['push']>['2']
|
||||
|
||||
/**
|
||||
* Don't use this inside a useEffect with `router` as a dependency (watch out for infinite loops).
|
||||
*/
|
||||
export const makeQuery = (
|
||||
router: NextRouter,
|
||||
queryParams: QueryParams,
|
||||
opts?: { replace?: boolean } & TransitionOptions
|
||||
) => {
|
||||
const url = getHrefWithQuery(router.asPath, queryParams)
|
||||
const replace = opts?.replace
|
||||
delete opts?.replace
|
||||
if (replace) return router.replace(url, url, { scroll: false, ...opts })
|
||||
else return router.push(url, url, { scroll: false, ...opts })
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import '~/css/global.scss'
|
||||
|
||||
import { NextComponentType, NextPageContext } from 'next'
|
||||
import { AppProps } from 'next/app'
|
||||
import { RealViewportProvider } from 'next-real-viewport'
|
||||
import * as React from 'react'
|
||||
|
||||
import { AnimationContextProvider } from '~/context/animation'
|
||||
import { basementLog, isProd } from '~/lib/constants'
|
||||
import { FontsReadyScript } from '~/lib/font-scripts'
|
||||
import { GAScripts, useAppGA } from '~/lib/ga'
|
||||
import { PageTransitionsProvider } from '~/lib/gsap/page-transitions'
|
||||
|
||||
export type Page<P = Record<string, unknown>> = NextComponentType<
|
||||
NextPageContext,
|
||||
Record<string, unknown>,
|
||||
P
|
||||
> & { getLayout?: GetLayoutFn<P> }
|
||||
|
||||
export type GetLayoutFn<P = Record<string, unknown>> = (
|
||||
props: AppProps<P>
|
||||
) => React.ReactNode
|
||||
|
||||
if (isProd) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(basementLog)
|
||||
}
|
||||
|
||||
const App = ({ Component, pageProps, ...rest }: AppProps) => {
|
||||
useAppGA()
|
||||
|
||||
React.useEffect(() => {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.code === `Tab`) {
|
||||
document.body.classList.add('user-is-tabbing')
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseDown() {
|
||||
document.body.classList.remove('user-is-tabbing')
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
window.addEventListener('mousedown', handleMouseDown)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
window.removeEventListener('mousedown', handleMouseDown)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getLayout: GetLayoutFn =
|
||||
(Component as any).getLayout ||
|
||||
(({ Component, pageProps }) => <Component {...pageProps} />)
|
||||
|
||||
return (
|
||||
<RealViewportProvider debounceResize={false}>
|
||||
<AnimationContextProvider>
|
||||
<PageTransitionsProvider>
|
||||
<GAScripts />
|
||||
<FontsReadyScript />
|
||||
{getLayout({ Component, pageProps, ...rest })}
|
||||
</PageTransitionsProvider>
|
||||
</AnimationContextProvider>
|
||||
</RealViewportProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,28 @@
|
||||
import Document, {
|
||||
DocumentContext,
|
||||
Head,
|
||||
Html,
|
||||
Main,
|
||||
NextScript
|
||||
} from 'next/document'
|
||||
|
||||
class MyDocument extends Document {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const initialProps = await Document.getInitialProps(ctx)
|
||||
return { ...initialProps }
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body style={{ opacity: 0 }}>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Meta } from '~/components/common/meta'
|
||||
import { PageLayout } from '~/components/layout/page'
|
||||
import Hero from '~/components/sections/homepage/hero'
|
||||
|
||||
import { Page } from './_app'
|
||||
|
||||
const HomePage: Page = () => {
|
||||
return (
|
||||
<PageLayout>
|
||||
<Meta />
|
||||
<Hero />
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
@@ -0,0 +1,4 @@
|
||||
# `ts` dir
|
||||
|
||||
- Put common types in here
|
||||
- Specific types used throughout the application are best placed where they are used (prefer local instead of global): only abstract them here if they are used more commonly.
|
||||
Reference in New Issue
Block a user