Initial Commit

This commit is contained in:
Nazareno Oviedo
2022-03-28 15:00:11 -03:00
commit 4aa3fdbc2a
147 changed files with 8710 additions and 0 deletions
+42
View File
@@ -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) })
}
+52
View File
@@ -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'
+35
View File
@@ -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} />
}
+75
View File
@@ -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])
}
+151
View File
@@ -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 }
+147
View File
@@ -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>
)
}
+106
View File
@@ -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
}
+36
View File
@@ -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>
)
})
+228
View File
@@ -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 }
}
+16
View File
@@ -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 }
}
+41
View File
@@ -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
}
+62
View File
@@ -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 })
}