mirror of
https://github.com/LaconicNetwork/laconic.com.git
synced 2026-09-09 01:04:08 +00:00
Initial Commit
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
Reference in New Issue
Block a user