load home data on client to speed up page load

This commit is contained in:
saml33 2024-03-06 11:28:05 +11:00
parent f7c07e989d
commit 19ead30b17
9 changed files with 668 additions and 775 deletions

View File

@ -1,9 +1,5 @@
import { Metadata } from 'next'
import HomePage from '../components/home/HomePage'
import { fetchAppStatsData, fetchMangoMarketData } from '../utils/mango'
import { fetchTokenPages } from '../../contentful/tokenPage'
import { draftMode } from 'next/headers'
import { fetchHomePageAnnouncements } from '../../contentful/homePageAnnouncement'
const metaTitle = 'Mango Markets | Safer. Smarter. Faster.'
const metaDescription =
@ -31,29 +27,9 @@ export const metadata: Metadata = {
}
async function Page() {
const appStatsDataPromise = fetchAppStatsData()
const marketsPromise = fetchMangoMarketData()
const tokensPromise = fetchTokenPages({
preview: draftMode().isEnabled,
})
const announcementsPromise = fetchHomePageAnnouncements({
preview: draftMode().isEnabled,
})
const [appStatsData, markets, tokens, announcements] = await Promise.all([
appStatsDataPromise,
marketsPromise,
tokensPromise,
announcementsPromise,
])
return (
<div>
<HomePage
announcements={announcements}
appStatsData={appStatsData}
markets={markets}
tokens={tokens}
/>
<HomePage />
</div>
)
}

View File

@ -1,76 +0,0 @@
import Image from 'next/image'
import { HomePageAnnouncement } from '../../../contentful/homePageAnnouncement'
// import { ChevronRightIcon } from '@heroicons/react/20/solid'
import { ReactNode } from 'react'
import Link from 'next/link'
import { usePlausible } from 'next-plausible'
import { useViewport } from '../../hooks/useViewport'
const classNames =
'border border-th-bkg-4 p-4 rounded-lg lg:h-32 lg:py-0 flex items-center justify-between md:hover:bg-th-bkg-3 lg:py-4'
const AnnouncementWrapper = ({
children,
isExternal,
path,
}: {
children: ReactNode
isExternal: boolean
path: string
}) => {
const telemetry = usePlausible()
const trackClick = () => {
telemetry('homeAnnouncement', {
props: {
path: path,
},
})
}
return isExternal ? (
<a
className={classNames}
href={path}
rel="noopener noreferrer"
onClick={trackClick}
>
{children}
</a>
) : (
<Link className={classNames} href={path} onClick={trackClick} shallow>
{children}
</Link>
)
}
const Announcement = ({ data }: { data: HomePageAnnouncement }) => {
const { category, linkPath, description, image, title } = data
const imageSrc = image?.src
const imageAlt = image?.alt || 'CTA Image'
const isExtenalLink = linkPath.includes('http')
const { isDesktop } = useViewport()
return (
<AnnouncementWrapper isExternal={isExtenalLink} path={linkPath}>
<span className="flex items-center space-x-3">
{imageSrc ? (
<Image
className="flex-shrink-0 rounded-full"
src={`https:${imageSrc}`}
alt={imageAlt}
height={isDesktop ? 64 : 48}
width={isDesktop ? 64 : 48}
/>
) : null}
<div>
<p className="mb-1 text-xs text-th-active leading-none">{category}</p>
<p className="text-th-fgd-2 text-lg block font-display">{title}</p>
<p className="text-sm block text-th-fgd-3">{description}</p>
</div>
</span>
{/* <ChevronRightIcon className="ml-3 h-6 w-6 text-th-fgd-4 flex-shrink-0" /> */}
</AnnouncementWrapper>
)
}
export default Announcement

View File

@ -0,0 +1,185 @@
'use client'
import Image from 'next/image'
import {
HomePageAnnouncement,
fetchHomePageAnnouncements,
} from '../../../contentful/homePageAnnouncement'
import Slider from 'react-slick'
import { ReactNode, useRef } from 'react'
import Link from 'next/link'
import { usePlausible } from 'next-plausible'
import { breakpoints, useViewport } from '../../hooks/useViewport'
import { useQuery } from '@tanstack/react-query'
import SectionWrapper from '../shared/SectionWrapper'
import {
ChevronLeftIcon,
ChevronRightIcon,
MegaphoneIcon,
} from '@heroicons/react/20/solid'
const classNames =
'border border-th-bkg-4 p-4 rounded-lg lg:h-32 lg:py-0 flex items-center justify-between md:hover:bg-th-bkg-3 lg:py-4'
const AnnouncementWrapper = ({
children,
isExternal,
path,
}: {
children: ReactNode
isExternal: boolean
path: string
}) => {
const telemetry = usePlausible()
const trackClick = () => {
telemetry('homeAnnouncement', {
props: {
path: path,
},
})
}
return isExternal ? (
<a
className={classNames}
href={path}
rel="noopener noreferrer"
onClick={trackClick}
>
{children}
</a>
) : (
<Link className={classNames} href={path} onClick={trackClick} shallow>
{children}
</Link>
)
}
const Announcement = ({ data }: { data: HomePageAnnouncement }) => {
const { category, linkPath, description, image, title } = data
const imageSrc = image?.src
const imageAlt = image?.alt || 'CTA Image'
const isExtenalLink = linkPath.includes('http')
const { isDesktop } = useViewport()
return (
<AnnouncementWrapper isExternal={isExtenalLink} path={linkPath}>
<span className="flex items-center space-x-3">
{imageSrc ? (
<Image
className="flex-shrink-0 rounded-full"
src={`https:${imageSrc}`}
alt={imageAlt}
height={isDesktop ? 64 : 48}
width={isDesktop ? 64 : 48}
/>
) : null}
<div>
<p className="mb-1 text-xs text-th-active leading-none">{category}</p>
<p className="text-th-fgd-2 text-lg block font-display">{title}</p>
<p className="text-sm block text-th-fgd-3">{description}</p>
</div>
</span>
</AnnouncementWrapper>
)
}
const Announcements = () => {
const { width } = useViewport()
const { data: announcements } = useQuery({
queryKey: ['announcements'],
queryFn: () => fetchHomePageAnnouncements({ preview: false }),
})
const sliderSettings = {
arrows: false,
dots: false,
infinite: true,
slidesToShow: 3,
slidesToScroll: 1,
cssEase: 'linear',
responsive: [
{
breakpoint: breakpoints.xl,
settings: {
slidesToShow: 2,
},
},
{
breakpoint: breakpoints.lg,
settings: {
slidesToShow: 1,
},
},
],
}
const sliderRef = useRef<Slider | null>(null)
const nextSlide = () => {
if (sliderRef.current) {
sliderRef.current.slickNext()
}
}
const prevSlide = () => {
if (sliderRef.current) {
sliderRef.current.slickPrev()
}
}
const slides = width >= breakpoints.xl ? 3 : width >= breakpoints.lg ? 2 : 1
const showArrows = announcements?.length
? slides < announcements.length
: false
return announcements?.length ? (
<SectionWrapper
className="mt-0 lg:mt-6 py-6 lg:py-12 bg-th-bkg-2 xl:rounded-xl px-6"
noPaddingY
noPaddingX={showArrows}
>
<div
className={`mb-6 flex items-center justify-center ${
showArrows ? 'md:px-6 lg:px-14' : ''
}`}
>
<div className="mr-3 flex items-center justify-center w-10 h-10 bg-th-active rounded-full">
<MegaphoneIcon className="w-6 h-6 text-th-bkg-1 -rotate-[30deg]" />
</div>
<h2 className="text-3xl">Latest news</h2>
</div>
<div className="flex items-center">
{showArrows ? (
<button
className="mr-4 flex items-center justify-center w-8 h-8 border-2 border-th-bkg-4 rounded-full"
onClick={prevSlide}
>
<ChevronLeftIcon className="w-5 h-5 text-th-fgd-1" />
</button>
) : null}
<div className={` ${showArrows ? 'w-[calc(100%-88px)]' : 'w-full'}`}>
<Slider ref={sliderRef} {...sliderSettings}>
{announcements.map((announcement, i) => (
<div
className={i !== announcements.length - 1 ? 'pr-3' : 'pr-[1px]'}
key={announcement.title + i}
>
<Announcement data={announcement} />
</div>
))}
</Slider>
</div>
{showArrows ? (
<button
className="ml-1 flex items-center justify-center w-8 h-8 border-2 border-th-bkg-4 rounded-full"
onClick={nextSlide}
>
<ChevronRightIcon className="w-5 h-5 text-th-fgd-1" />
</button>
) : null}
</div>
</SectionWrapper>
) : null
}
export default Announcements

View File

@ -0,0 +1,26 @@
'use client'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
const BlackSphere = () => {
const { theme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return <div className="sphere w-56 h-56" />
return (
<img
className={`sphere absolute -left-6 sm:left-6 w-56 h-auto xl:-left-12 ${
theme === 'Dark' ? '' : 'opacity-0'
}`}
src="/images/new/black-sphere.png"
alt=""
/>
)
}
export default BlackSphere

View File

@ -1,63 +1,25 @@
'use client'
import {
ArrowPathRoundedSquareIcon,
BoltIcon,
BuildingLibraryIcon,
ChevronLeftIcon,
ChevronRightIcon,
CurrencyDollarIcon,
DevicePhoneMobileIcon,
FaceFrownIcon,
MegaphoneIcon,
NoSymbolIcon,
QuestionMarkCircleIcon,
RocketLaunchIcon,
} from '@heroicons/react/20/solid'
import LiquidIcon from '../icons/LiquidIcon'
import ButtonLink from '../shared/ButtonLink'
import IconWithText from '../shared/IconWithText'
import SectionWrapper from '../shared/SectionWrapper'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import {
HTMLProps,
ReactNode,
forwardRef,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import { MotionPathPlugin } from 'gsap/dist/MotionPathPlugin'
import ColorBlur from '../shared/ColorBlur'
import Ottersec from '../icons/Ottersec'
import { formatNumericValue, numberCompacter } from '../../utils/numbers'
import HeroStat from './HeroStat'
import { AppStatsData, FormattedMarketData } from '../../types/mango'
import { useTheme } from 'next-themes'
import { TokenPageWithData } from '../../../contentful/tokenPage'
import { CUSTOM_TOKEN_ICONS } from '../../utils/constants'
import Image from 'next/image'
import { useQuery } from '@tanstack/react-query'
import { fetchMangoTokensData } from '../../utils/mango'
import Link from 'next/link'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import SheenLoader from '../shared/SheenLoader'
import { HomePageAnnouncement } from '../../../contentful/homePageAnnouncement'
import Announcement from './Announcement'
import Slider from 'react-slick'
import { breakpoints, useViewport } from '../../hooks/useViewport'
dayjs.extend(relativeTime)
import Markets from './Markets'
import Announcements from './Announcements'
import { BuildWrapper, SwapStageWrapper } from './Wrappers'
import BlackSphere from './BlackSphere'
type Markets = {
perp: FormattedMarketData[]
spot: FormattedMarketData[]
}
gsap.registerPlugin(MotionPathPlugin)
gsap.registerPlugin(ScrollTrigger)
// type Markets = {
// perp: FormattedMarketData[]
// spot: FormattedMarketData[]
// }
const tokenIcons = [
{ icon: 'coin-orange.png', x: '10%', y: '20%' },
@ -69,7 +31,7 @@ const tokenIcons = [
{ icon: 'ring-white.png', x: '88%', y: '40%' },
{ icon: 'coin-silver.png', x: '86%', y: '48%' },
{ icon: 'cube-pink.png', x: '10%', y: '73%' },
{ icon: 'spring-chrome.png', x: '14%', y: '78%' },
{ icon: 'spring-chrome.png', x: '14%', y: '83%' },
{ icon: 'pyramid-blue.png', x: '12%', y: '40%' },
{ icon: 'coin-orange.png', x: '81%', y: '68%' },
]
@ -77,263 +39,14 @@ const tokenIcons = [
const MOBILE_IMAGE_CLASSES =
'core-image h-[240px] w-[240px] sm:h-[300px] sm:w-[300px] md:h-[480px] md:w-[480px] mb-6 lg:mb-0'
const HomePage = ({
announcements,
appStatsData,
markets,
tokens,
}: {
announcements: HomePageAnnouncement[]
appStatsData: AppStatsData
markets: Markets
tokens: TokenPageWithData[]
}) => {
const topSection = useRef<HTMLDivElement>(null)
const callouts = useRef<HTMLDivElement>(null)
const swapPanel = useRef<HTMLDivElement>(null)
const coreFeatures = useRef<HTMLDivElement>(null)
const build = useRef<HTMLDivElement>(null)
const { width } = useViewport()
const numberOfMarkets =
(markets?.spot.length || 0) + (markets?.perp.length || 0)
const [gainers, losers] = useMemo(() => {
if (!tokens?.length) return [[], []]
const sortedTokens = tokens.sort((a, b) => {
const aChange = a?.birdeyePrices?.length
? ((a.birdeyePrices[a.birdeyePrices.length - 1].value -
a.birdeyePrices[0].value) /
a.birdeyePrices[0].value) *
100
: a?.birdeyeData?.priceChange24hPercent
? a.birdeyeData.priceChange24hPercent
: 0
const bChange = b?.birdeyePrices?.length
? ((b.birdeyePrices[b.birdeyePrices.length - 1].value -
b.birdeyePrices[0].value) /
b.birdeyePrices[0].value) *
100
: b?.birdeyeData?.priceChange24hPercent
? b.birdeyeData.priceChange24hPercent
: 0
return bChange - aChange
})
const gainers = sortedTokens.slice(0, 5).filter((token) => {
const { birdeyeData, birdeyePrices } = token
const change = birdeyePrices?.length
? ((birdeyePrices[birdeyePrices.length - 1].value -
birdeyePrices[0].value) /
birdeyePrices[0].value) *
100
: birdeyeData?.priceChange24hPercent
? birdeyeData.priceChange24hPercent
: 0
return change > 0
})
const losers = sortedTokens.slice(-5).filter((token) => {
const { birdeyeData, birdeyePrices } = token
const change = birdeyePrices?.length
? ((birdeyePrices[birdeyePrices.length - 1].value -
birdeyePrices[0].value) /
birdeyePrices[0].value) *
100
: birdeyeData?.priceChange24hPercent
? birdeyeData.priceChange24hPercent
: 0
return change < 0
})
return [gainers, losers.reverse()]
}, [tokens])
const formattedAppStatsData = useMemo(() => {
if (!appStatsData || !Object.keys(appStatsData).length)
return { totalVol24h: 0, totalTrades24h: 0, weeklyActiveTraders: 0 }
// volume
const spotVol24h = appStatsData?.openbook_volume_usd_24h || 0
const perpVol24h = appStatsData?.perp_volume_usd_24h || 0
const swapVol24h = appStatsData?.swap_volume_usd_24h || 0
const totalVol24h = spotVol24h + perpVol24h + swapVol24h
// number of trades
const spotTrades24h = appStatsData?.num_openbook_fills_24h || 0
const perpTrades24h = appStatsData?.num_perp_fills_24h || 0
const swapTrades24h = appStatsData?.num_swaps_24h || 0
const totalTrades24h = spotTrades24h + perpTrades24h + swapTrades24h
const weeklyActiveTraders = appStatsData?.weekly_active_mango_accounts || 0
return { totalVol24h, totalTrades24h, weeklyActiveTraders }
}, [appStatsData])
useLayoutEffect(() => {
const ctx = gsap.context((self) => {
if (self?.selector) {
const boxes = self.selector('.highlight-features')
boxes.forEach((box) => {
gsap.to(box, {
opacity: 1,
y: -40,
ease: 'power3.inOut',
scrollTrigger: {
trigger: box,
end: 'top 40%',
scrub: true,
},
})
})
}
}, callouts) // <- Scope!
return () => ctx.revert() // <- Cleanup!
}, [])
useLayoutEffect(() => {
const ctx = gsap.context((self) => {
if (self?.selector) {
const icons = self.selector('.token-icon')
icons.forEach((icon, i) => {
gsap.to(icon, {
y: i % 2 ? 100 : -100,
rotateZ: i % 2 ? 45 : -45,
scrollTrigger: {
trigger: icon,
scrub: true,
},
})
})
gsap
.timeline({
scrollTrigger: {
trigger: '#swap-desktop',
scrub: true,
},
})
.from('#swap-desktop', {
rotateX: -45,
})
}
}, swapPanel) // <- Scope!
return () => ctx.revert() // <- Cleanup!
}, [])
useLayoutEffect(() => {
const ctx = gsap.context((self) => {
if (self?.selector) {
const features = self.selector('.core-feature')
const text = self.selector('.core-text')
const image = self.selector('.core-image')
features.forEach((feature, i) => {
gsap.from(text[i], {
opacity: 0.4,
y: 60,
ease: 'power3.inOut',
scrollTrigger: {
start: 'top 60%',
end: 'top 20%',
trigger: feature,
scrub: true,
},
})
gsap.from(image[i], {
opacity: 0.4,
scale: 0.9,
ease: 'power3.inOut',
scrollTrigger: {
start: 'top 60%',
end: 'top 20%',
trigger: feature,
scrub: true,
},
})
})
}
}, coreFeatures) // <- Scope!
return () => ctx.revert() // <- Cleanup!
}, [])
useLayoutEffect(() => {
const ctx = gsap.context((self) => {
if (self?.selector) {
const spheres = self.selector('.sphere')
spheres.forEach((sphere, i) => {
gsap.to(sphere, {
y: i % 2 ? -150 : 100,
scrollTrigger: {
trigger: sphere,
start: i % 2 ? 'bottom bottom' : 'center center',
scrub: true,
},
})
})
}
}, topSection) // <- Scope!
return () => ctx.revert() // <- Cleanup!
}, [])
useLayoutEffect(() => {
const ctx = gsap.context(() => {
gsap
.timeline({
scrollTrigger: {
trigger: '#build',
scrub: true,
},
})
.from('#build', {
y: -200,
})
}, build) // <- Scope!
return () => ctx.revert() // <- Cleanup!
}, [])
const sliderSettings = {
arrows: false,
dots: false,
infinite: true,
slidesToShow: 3,
slidesToScroll: 1,
cssEase: 'linear',
responsive: [
{
breakpoint: breakpoints.xl,
settings: {
slidesToShow: 2,
},
},
{
breakpoint: breakpoints.lg,
settings: {
slidesToShow: 1,
},
},
],
}
const sliderRef = useRef<Slider | null>(null)
const nextSlide = () => {
if (sliderRef.current) {
sliderRef.current.slickNext()
}
}
const prevSlide = () => {
if (sliderRef.current) {
sliderRef.current.slickPrev()
}
}
const slides = width >= breakpoints.xl ? 3 : width >= breakpoints.lg ? 2 : 1
const showArrows = announcements?.length
? slides < announcements.length
: false
const HomePage = () => {
return (
<>
<SectionWrapper
className="overflow-hidden h-[760px] lg:h-[600px] lg:flex lg:items-center py-12 lg:py-0"
noPaddingY
>
<div className="grid grid-cols-12" ref={topSection}>
<div className="grid grid-cols-12">
<div className="col-span-12 lg:col-span-5 mb-12 lg:mb-0 relative z-10">
<h1 className="mb-6 text-center lg:text-left">
Safer. Smarter. Faster.
@ -365,63 +78,12 @@ const HomePage = ({
</div>
</div>
</SectionWrapper>
{announcements?.length ? (
<SectionWrapper
className="mt-0 lg:mt-6 py-6 lg:py-12 bg-th-bkg-2 xl:rounded-xl px-6"
noPaddingY
noPaddingX={showArrows}
>
<div
className={`mb-6 flex items-center justify-center ${
showArrows ? 'md:px-6 lg:px-14' : ''
}`}
>
<div className="mr-3 flex items-center justify-center w-10 h-10 bg-th-active rounded-full">
<MegaphoneIcon className="w-6 h-6 text-th-bkg-1 -rotate-[30deg]" />
</div>
<h2 className="text-3xl">Latest news</h2>
</div>
<div className="flex items-center">
{showArrows ? (
<button
className="mr-4 flex items-center justify-center w-8 h-8 border-2 border-th-bkg-4 rounded-full"
onClick={prevSlide}
>
<ChevronLeftIcon className="w-5 h-5 text-th-fgd-1" />
</button>
) : null}
<div
className={` ${showArrows ? 'w-[calc(100%-88px)]' : 'w-full'}`}
>
<Slider ref={sliderRef} {...sliderSettings}>
{announcements.map((announcement, i) => (
<div
className={
i !== announcements.length - 1 ? 'pr-3' : 'pr-[1px]'
}
key={announcement.title + i}
>
<Announcement data={announcement} />
</div>
))}
</Slider>
</div>
{showArrows ? (
<button
className="ml-1 flex items-center justify-center w-8 h-8 border-2 border-th-bkg-4 rounded-full"
onClick={nextSlide}
>
<ChevronRightIcon className="w-5 h-5 text-th-fgd-1" />
</button>
) : null}
</div>
</SectionWrapper>
) : null}
<Announcements />
<SectionWrapper className="mt-10 md:mt-0">
<div className="grid grid-cols-6 gap-4 lg:gap-6" ref={callouts}>
<div className="-mt-10 mb-12 lg:mb-16 col-span-6">
<h2 className="text-center">DeFi maxed</h2>
</div>
<div className="mb-8 md:mb-10">
<h2 className="text-center">DeFi maxed</h2>
</div>
<div className="grid grid-cols-6 gap-4 lg:gap-6">
<IconWithText
desc="Low fees for taker trades and rebates for maker trades. Plus, Solana's extremely low transaction costs."
icon={
@ -470,47 +132,15 @@ const HomePage = ({
/>
</div>
</SectionWrapper>
<SectionWrapper className="-mt-10 pb-12 md:pb-24 lg:pb-32" noPaddingY>
<div className="flex items-center justify-between mb-8 md:mb-10">
<h2>Markets</h2>
<ButtonLink path="/explore/tokens" linkText="Explore" size="large" />
</div>
<div className="grid grid-cols-3 gap-4 lg:gap-6">
<HeroStat title="Markets" value={numberOfMarkets.toString()} />
{/* <HeroStat
title="Active Traders"
tooltipContent="Weekly active Mango Accounts"
value={formatNumericValue(
formattedAppStatsData.weeklyActiveTraders,
)}
/> */}
<HeroStat
title="Daily Volume"
tooltipContent="Volume across spot, swap and perp"
value={`$${numberCompacter.format(
formattedAppStatsData.totalVol24h,
)}`}
/>
<HeroStat
title="Daily Trades"
tooltipContent="Number of trades across spot, swap and perp"
value={formatNumericValue(formattedAppStatsData.totalTrades24h)}
/>
</div>
<div className="grid grid-cols-3 gap-4 lg:gap-6 mt-4 lg:mt-6">
<RecentlyListed tokens={tokens} />
<GainersLosers tokens={gainers} isGainers />
<GainersLosers tokens={losers} />
</div>
</SectionWrapper>
<SwapStageWrapper ref={swapPanel}>
<Markets />
<SwapStageWrapper>
<SectionWrapper className="relative overflow-hidden">
<ColorBlur
className="-top-20 left-0 -rotate-25 opacity-20"
height="800px"
width="600px"
/>
<div className="w-full h-full" ref={swapPanel}>
<div className="w-full h-full">
<div className="relative min-h-[580px] md:min-h-[640px] lg:min-h-[730px]">
<div className="relative z-10">
<h2 className="mb-4 text-center">Level up your swaps</h2>
@ -550,7 +180,7 @@ const HomePage = ({
</SectionWrapper>
</SwapStageWrapper>
<SectionWrapper>
<div className="flex flex-col justify-center" ref={coreFeatures}>
<div className="flex flex-col justify-center">
<div className="h-full xl:px-12">
<div className="core-feature flex flex-col lg:flex-row lg:items-center pb-12 lg:pb-24">
<img
@ -645,7 +275,7 @@ const HomePage = ({
</div>
</div>
</SectionWrapper>
<BuildWrapper ref={build}>
<BuildWrapper>
<SectionWrapper className="relative overflow-hidden">
<div className="absolute -bottom-40 left-1/2 -translate-x-1/2 bg-gradient-to-tl shadow-xl from-th-bkg-1 to-th-bkg-2 h-[600px] w-[600px] md:h-[800px] md:w-[800px] rounded-full" />
<ColorBlur
@ -653,7 +283,7 @@ const HomePage = ({
height="600px"
width="600px"
/>
<div className="flex flex-col items-center relative" ref={build}>
<div className="flex flex-col items-center relative">
<div className="relative h-[280px] w-[180px] md:h-[380px] md:w-[300px] mb-4 md:mb-8">
<img
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-40 md:w-56 h-auto"
@ -685,283 +315,3 @@ const HomePage = ({
}
export default HomePage
const BlackSphere = () => {
const { theme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return <div className="sphere w-56 h-56" />
return (
<img
className={`sphere absolute -left-6 sm:left-6 w-56 h-auto xl:-left-12 ${
theme === 'Dark' ? '' : 'opacity-0'
}`}
src="/images/new/black-sphere.png"
alt=""
/>
)
}
interface RefWrapperProps extends HTMLProps<HTMLDivElement> {
children: ReactNode
}
const SwapStageWrapper = forwardRef<HTMLDivElement, RefWrapperProps>(
(props, ref) => {
const { theme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return <div>{props.children}</div>
return (
<div
className={`${
theme === 'Dark'
? `bg-[url('/images/new/stage-slice.png')]`
: `bg-[url('/images/new/stage-slice-light.png')]`
} bg-repeat-x bg-contain`}
ref={ref}
>
{props.children}
</div>
)
},
)
const BuildWrapper = forwardRef<HTMLDivElement, RefWrapperProps>(
(props, ref) => {
const { theme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return <div className="bg-th-bkg-2">{props.children}</div>
return (
<div
className={`${
theme === 'Dark'
? `bg-[url('/images/new/cube-bg.png')]`
: `bg-[url('/images/new/cube-bg-light.png')]`
} bg-repeat`}
ref={ref}
>
{props.children}
</div>
)
},
)
const RecentlyListed = ({ tokens }: { tokens: TokenPageWithData[] }) => {
const { data: mangoTokensData, isLoading: loadingMangoTokensData } = useQuery(
{
queryKey: ['recently-listed-data'],
queryFn: () => fetchMangoTokensData(),
},
)
const recentlyListed = useMemo(() => {
if (!mangoTokensData?.length) return []
return mangoTokensData
.sort((a, b) => {
const dateA = a?.listing_time
? new Date(a.listing_time).getTime()
: -Infinity
const dateB = b?.listing_time
? new Date(b.listing_time).getTime()
: -Infinity
return dateB - dateA
})
.slice(0, 5)
}, [mangoTokensData])
return (
<div className="border border-th-bkg-3 rounded-xl p-8 col-span-3 lg:col-span-1">
<h3 className="mb-3 text-lg">Recently Listed</h3>
{loadingMangoTokensData ? (
<div className="space-y-1 mt-2">
{[...Array(5)].map((x, i) => (
<SheenLoader className="flex flex-1" key={i}>
<div className="h-14 w-full bg-th-bkg-2" />
</SheenLoader>
))}
</div>
) : recentlyListed.length ? (
recentlyListed.map((token) => {
const { listing_time, symbol } = token
const tokenPageData = tokens?.find(
(tkn) => tkn?.symbol?.toLowerCase() === symbol?.toLowerCase(),
)
const hasCustomIcon = symbol
? CUSTOM_TOKEN_ICONS[symbol.toLowerCase()]
: false
const logoPath = hasCustomIcon
? `/icons/tokens/${symbol?.toLowerCase()}.svg`
: ''
return (
<Link
href={
tokenPageData?.slug
? `/explore/tokens/${tokenPageData.slug}`
: '/explore/tokens'
}
key={symbol}
className="flex items-center justify-between py-2 md:hover:bg-th-bkg-2 -mx-3 rounded-lg"
shallow
>
<div className="flex items-center space-x-3 pl-3">
{logoPath ? (
<Image
className="rounded-full"
src={logoPath}
alt="Logo"
height={32}
width={32}
/>
) : (
<QuestionMarkCircleIcon className="h-8 w-8 text-th-fgd-4" />
)}
<div>
<p className="font-bold text-th-fgd-1">{symbol}</p>
{tokenPageData?.tokenName ? (
<p className="text-sm">{tokenPageData.tokenName}</p>
) : null}
</div>
</div>
<div className="flex items-center space-x-3 pr-2">
<div>
{listing_time ? (
<p className="text-right text-xs text-th-fgd-3">
{dayjs().to(listing_time)}
</p>
) : null}
</div>
<ChevronRightIcon className="h-6 w-6 text-th-fgd-4" />
{/* <ButtonLink
path={`https://app.mango.markets/trade?name=${symbol}/USDC`}
linkText="Trade"
secondary
/> */}
</div>
</Link>
)
})
) : (
<div className="flex items-center justify-center h-full -mt-8">
<div className="flex flex-col items-center py-10">
<NoSymbolIcon className="mb-2 h-8 w-8 text-th-down" />
<p>Failed to fetch recently listed tokens</p>
</div>
</div>
)}
</div>
)
}
const GainersLosers = ({
tokens,
isGainers,
}: {
tokens: TokenPageWithData[]
isGainers?: boolean
}) => {
return (
<div className="border border-th-bkg-3 rounded-xl p-8 col-span-3 lg:col-span-1">
<h3 className="mb-3 text-lg">{isGainers ? 'Gainers' : 'Losers'}</h3>
{tokens.length ? (
tokens.map((token) => {
const {
birdeyeData,
birdeyePrices,
symbol,
slug,
// spotSymbol,
tokenName,
} = token
const hasCustomIcon = symbol
? CUSTOM_TOKEN_ICONS[symbol.toLowerCase()]
: false
const logoPath = hasCustomIcon
? `/icons/tokens/${symbol?.toLowerCase()}.svg`
: birdeyeData?.logoURI
const change = birdeyePrices?.length
? ((birdeyePrices[birdeyePrices.length - 1].value -
birdeyePrices[0].value) /
birdeyePrices[0].value) *
100
: birdeyeData?.priceChange24hPercent
? birdeyeData.priceChange24hPercent
: 0
return (
<Link
href={`/explore/tokens/${slug}`}
key={slug}
className="flex items-center justify-between py-2 md:hover:bg-th-bkg-2 -mx-3 rounded-lg"
shallow
>
<div className="flex items-center space-x-3 pl-3">
{logoPath ? (
<Image
className="rounded-full"
src={logoPath}
alt="Logo"
height={32}
width={32}
/>
) : (
<QuestionMarkCircleIcon className="h-8 w-8 text-th-fgd-4" />
)}
<div>
<p className="font-bold text-th-fgd-1">{symbol}</p>
<p className="text-sm">{tokenName}</p>
</div>
</div>
<div className="flex items-center space-x-3 pr-2">
<div>
<p
className={`text-right ${
isGainers ? 'text-th-up' : 'text-th-down'
}`}
>
{`${change.toFixed(2)}%`}
</p>
<p className="text-right text-th-fgd-1 text-sm">
{birdeyeData?.price
? `$${formatNumericValue(birdeyeData.price)}`
: ''}
</p>
</div>
<ChevronRightIcon className="h-6 w-6 text-th-fgd-4" />
{/* <ButtonLink
path={`https://app.mango.markets/trade?name=${spotSymbol}/USDC`}
linkText="Trade"
secondary
/> */}
</div>
</Link>
)
})
) : (
<div className="flex items-center justify-center h-full -mt-8">
<div className="flex flex-col items-center py-10">
{isGainers ? (
<FaceFrownIcon className="mb-2 h-8 w-8 text-th-active" />
) : (
<RocketLaunchIcon className="mb-2 h-8 w-8 text-th-active" />
)}
<p>{`No ${isGainers ? 'gainers' : 'losers'}`}</p>
</div>
</div>
)}
</div>
)
}

View File

@ -0,0 +1,374 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import {
TokenPageWithData,
fetchTokenPages,
} from '../../../contentful/tokenPage'
import ButtonLink from '../shared/ButtonLink'
import SectionWrapper from '../shared/SectionWrapper'
import HeroStat from './HeroStat'
import {
fetchAppStatsData,
fetchMangoMarketData,
fetchMangoTokensData,
} from '../../utils/mango'
import { useMemo } from 'react'
import SheenLoader from '../shared/SheenLoader'
import { CUSTOM_TOKEN_ICONS } from '../../utils/constants'
import Link from 'next/link'
import Image from 'next/image'
import {
ChevronRightIcon,
FaceFrownIcon,
NoSymbolIcon,
QuestionMarkCircleIcon,
RocketLaunchIcon,
} from '@heroicons/react/20/solid'
import { formatNumericValue, numberCompacter } from '../../utils/numbers'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime)
const Markets = () => {
const { data: markets, isLoading: loadingMarkets } = useQuery({
queryKey: ['markets-data'],
queryFn: () => fetchMangoMarketData(),
})
const { data: appStatsData, isLoading: loadingAppStatsData } = useQuery({
queryKey: ['app-stats-data'],
queryFn: () => fetchAppStatsData(),
})
const { data: tokens, isLoading: loadingTokens } = useQuery({
queryKey: ['cms-tokens-data'],
queryFn: () => fetchTokenPages({ preview: false }),
})
const numberOfMarkets =
(markets?.spot.length || 0) + (markets?.perp.length || 0)
const [gainers, losers] = useMemo(() => {
if (!tokens?.length) return [[], []]
const sortedTokens = tokens.sort((a, b) => {
const aChange = a?.birdeyePrices?.length
? ((a.birdeyePrices[a.birdeyePrices.length - 1].value -
a.birdeyePrices[0].value) /
a.birdeyePrices[0].value) *
100
: a?.birdeyeData?.priceChange24hPercent
? a.birdeyeData.priceChange24hPercent
: 0
const bChange = b?.birdeyePrices?.length
? ((b.birdeyePrices[b.birdeyePrices.length - 1].value -
b.birdeyePrices[0].value) /
b.birdeyePrices[0].value) *
100
: b?.birdeyeData?.priceChange24hPercent
? b.birdeyeData.priceChange24hPercent
: 0
return bChange - aChange
})
const gainers = sortedTokens.slice(0, 5).filter((token) => {
const { birdeyeData, birdeyePrices } = token
const change = birdeyePrices?.length
? ((birdeyePrices[birdeyePrices.length - 1].value -
birdeyePrices[0].value) /
birdeyePrices[0].value) *
100
: birdeyeData?.priceChange24hPercent
? birdeyeData.priceChange24hPercent
: 0
return change > 0
})
const losers = sortedTokens.slice(-5).filter((token) => {
const { birdeyeData, birdeyePrices } = token
const change = birdeyePrices?.length
? ((birdeyePrices[birdeyePrices.length - 1].value -
birdeyePrices[0].value) /
birdeyePrices[0].value) *
100
: birdeyeData?.priceChange24hPercent
? birdeyeData.priceChange24hPercent
: 0
return change < 0
})
return [gainers, losers.reverse()]
}, [tokens])
const formattedAppStatsData = useMemo(() => {
if (!appStatsData || !Object.keys(appStatsData).length)
return { totalVol24h: 0, totalTrades24h: 0, weeklyActiveTraders: 0 }
// volume
const spotVol24h = appStatsData?.openbook_volume_usd_24h || 0
const perpVol24h = appStatsData?.perp_volume_usd_24h || 0
const swapVol24h = appStatsData?.swap_volume_usd_24h || 0
const totalVol24h = spotVol24h + perpVol24h + swapVol24h
// number of trades
const spotTrades24h = appStatsData?.num_openbook_fills_24h || 0
const perpTrades24h = appStatsData?.num_perp_fills_24h || 0
const swapTrades24h = appStatsData?.num_swaps_24h || 0
const totalTrades24h = spotTrades24h + perpTrades24h + swapTrades24h
const weeklyActiveTraders = appStatsData?.weekly_active_mango_accounts || 0
return { totalVol24h, totalTrades24h, weeklyActiveTraders }
}, [appStatsData])
return (
<SectionWrapper className="pb-12 md:pb-24 lg:pb-32" noPaddingY>
<div className="flex items-center justify-between mb-8 md:mb-10">
<h2>Markets</h2>
<ButtonLink path="/explore/tokens" linkText="Explore" size="large" />
</div>
<div className="grid grid-cols-3 gap-4 lg:gap-6">
<HeroStat
title="Markets"
value={numberOfMarkets.toString()}
loading={loadingMarkets}
/>
<HeroStat
title="Daily Volume"
tooltipContent="Volume across spot, swap and perp"
value={`$${numberCompacter.format(
formattedAppStatsData.totalVol24h,
)}`}
loading={loadingAppStatsData}
/>
<HeroStat
title="Daily Trades"
tooltipContent="Number of trades across spot, swap and perp"
value={formatNumericValue(formattedAppStatsData.totalTrades24h)}
loading={loadingAppStatsData}
/>
</div>
<div className="grid grid-cols-3 gap-4 lg:gap-6 mt-4 lg:mt-6">
<RecentlyListed tokens={tokens} />
<GainersLosers tokens={gainers} isGainers loading={loadingTokens} />
<GainersLosers tokens={losers} loading={loadingTokens} />
</div>
</SectionWrapper>
)
}
export default Markets
const RecentlyListed = ({
tokens,
}: {
tokens: TokenPageWithData[] | undefined
}) => {
const { data: mangoTokensData, isLoading: loadingMangoTokensData } = useQuery(
{
queryKey: ['recently-listed-data'],
queryFn: () => fetchMangoTokensData(),
},
)
const recentlyListed = useMemo(() => {
if (!mangoTokensData?.length) return []
return mangoTokensData
.sort((a, b) => {
const dateA = a?.listing_time
? new Date(a.listing_time).getTime()
: -Infinity
const dateB = b?.listing_time
? new Date(b.listing_time).getTime()
: -Infinity
return dateB - dateA
})
.slice(0, 5)
}, [mangoTokensData])
return (
<div className="border border-th-bkg-3 rounded-xl p-8 col-span-3 lg:col-span-1">
<h3 className="mb-3 text-lg">Recently Listed</h3>
{loadingMangoTokensData ? (
<div className="space-y-1 mt-2">
{[...Array(5)].map((x, i) => (
<SheenLoader className="flex flex-1" key={i}>
<div className="h-14 w-full bg-th-bkg-2" />
</SheenLoader>
))}
</div>
) : recentlyListed.length ? (
recentlyListed.map((token) => {
const { listing_time, symbol } = token
const tokenPageData = tokens?.find(
(tkn) => tkn?.symbol?.toLowerCase() === symbol?.toLowerCase(),
)
const hasCustomIcon = symbol
? CUSTOM_TOKEN_ICONS[symbol.toLowerCase()]
: false
const logoPath = hasCustomIcon
? `/icons/tokens/${symbol?.toLowerCase()}.svg`
: ''
return (
<Link
href={
tokenPageData?.slug
? `/explore/tokens/${tokenPageData.slug}`
: '/explore/tokens'
}
key={symbol}
className="flex items-center justify-between py-2 md:hover:bg-th-bkg-2 -mx-3 rounded-lg"
shallow
>
<div className="flex items-center space-x-3 pl-3">
{logoPath ? (
<Image
className="rounded-full"
src={logoPath}
alt="Logo"
height={32}
width={32}
/>
) : (
<QuestionMarkCircleIcon className="h-8 w-8 text-th-fgd-4" />
)}
<div>
<p className="font-bold text-th-fgd-1">{symbol}</p>
{tokenPageData?.tokenName ? (
<p className="text-sm">{tokenPageData.tokenName}</p>
) : null}
</div>
</div>
<div className="flex items-center space-x-3 pr-2">
<div>
{listing_time ? (
<p className="text-right text-xs text-th-fgd-3">
{dayjs().to(listing_time)}
</p>
) : null}
</div>
<ChevronRightIcon className="h-6 w-6 text-th-fgd-4" />
{/* <ButtonLink
path={`https://app.mango.markets/trade?name=${symbol}/USDC`}
linkText="Trade"
secondary
/> */}
</div>
</Link>
)
})
) : (
<div className="flex items-center justify-center h-full -mt-8">
<div className="flex flex-col items-center py-10">
<NoSymbolIcon className="mb-2 h-8 w-8 text-th-down" />
<p>Failed to fetch recently listed tokens</p>
</div>
</div>
)}
</div>
)
}
const GainersLosers = ({
tokens,
loading,
isGainers,
}: {
tokens: TokenPageWithData[]
loading: boolean
isGainers?: boolean
}) => {
return (
<div className="border border-th-bkg-3 rounded-xl p-8 col-span-3 lg:col-span-1">
<h3 className="mb-3 text-lg">{isGainers ? 'Gainers' : 'Losers'}</h3>
{loading ? (
<div className="space-y-1 mt-2">
{[...Array(5)].map((x, i) => (
<SheenLoader className="flex flex-1" key={i}>
<div className="h-14 w-full bg-th-bkg-2" />
</SheenLoader>
))}
</div>
) : tokens.length ? (
tokens.map((token) => {
const {
birdeyeData,
birdeyePrices,
symbol,
slug,
// spotSymbol,
tokenName,
} = token
const hasCustomIcon = symbol
? CUSTOM_TOKEN_ICONS[symbol.toLowerCase()]
: false
const logoPath = hasCustomIcon
? `/icons/tokens/${symbol?.toLowerCase()}.svg`
: birdeyeData?.logoURI
const change = birdeyePrices?.length
? ((birdeyePrices[birdeyePrices.length - 1].value -
birdeyePrices[0].value) /
birdeyePrices[0].value) *
100
: birdeyeData?.priceChange24hPercent
? birdeyeData.priceChange24hPercent
: 0
return (
<Link
href={`/explore/tokens/${slug}`}
key={slug}
className="flex items-center justify-between py-2 md:hover:bg-th-bkg-2 -mx-3 rounded-lg"
shallow
>
<div className="flex items-center space-x-3 pl-3">
{logoPath ? (
<Image
className="rounded-full"
src={logoPath}
alt="Logo"
height={32}
width={32}
/>
) : (
<QuestionMarkCircleIcon className="h-8 w-8 text-th-fgd-4" />
)}
<div>
<p className="font-bold text-th-fgd-1">{symbol}</p>
<p className="text-sm">{tokenName}</p>
</div>
</div>
<div className="flex items-center space-x-3 pr-2">
<div>
<p
className={`text-right ${
isGainers ? 'text-th-up' : 'text-th-down'
}`}
>
{`${change.toFixed(2)}%`}
</p>
<p className="text-right text-th-fgd-1 text-sm">
{birdeyeData?.price
? `$${formatNumericValue(birdeyeData.price)}`
: ''}
</p>
</div>
<ChevronRightIcon className="h-6 w-6 text-th-fgd-4" />
{/* <ButtonLink
path={`https://app.mango.markets/trade?name=${spotSymbol}/USDC`}
linkText="Trade"
secondary
/> */}
</div>
</Link>
)
})
) : (
<div className="flex items-center justify-center h-full -mt-8">
<div className="flex flex-col items-center py-10">
{isGainers ? (
<FaceFrownIcon className="mb-2 h-8 w-8 text-th-active" />
) : (
<RocketLaunchIcon className="mb-2 h-8 w-8 text-th-active" />
)}
<p>{`No ${isGainers ? 'gainers' : 'losers'}`}</p>
</div>
</div>
)}
</div>
)
}

View File

@ -0,0 +1,58 @@
'use client'
import { useTheme } from 'next-themes'
import { HTMLProps, ReactNode, forwardRef, useEffect, useState } from 'react'
interface RefWrapperProps extends HTMLProps<HTMLDivElement> {
children: ReactNode
}
export const SwapStageWrapper = forwardRef<HTMLDivElement, RefWrapperProps>(
(props, ref) => {
const { theme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return <div>{props.children}</div>
return (
<div
className={`${
theme === 'Dark'
? `bg-[url('/images/new/stage-slice.png')]`
: `bg-[url('/images/new/stage-slice-light.png')]`
} bg-repeat-x bg-contain`}
ref={ref}
>
{props.children}
</div>
)
},
)
export const BuildWrapper = forwardRef<HTMLDivElement, RefWrapperProps>(
(props, ref) => {
const { theme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return <div className="bg-th-bkg-2">{props.children}</div>
return (
<div
className={`${
theme === 'Dark'
? `bg-[url('/images/new/cube-bg.png')]`
: `bg-[url('/images/new/cube-bg-light.png')]`
} bg-repeat`}
ref={ref}
>
{props.children}
</div>
)
},
)

View File

@ -21,7 +21,7 @@ const IconWithText = ({
}) => {
return (
<div
className={`highlight-features opacity-0 translate-y-[40px] col-span-6 sm:col-span-3 md:col-span-2 ${
className={`highlight-features col-span-6 sm:col-span-3 md:col-span-2 ${
showBackground ? 'border border-th-bkg-4 rounded-xl p-6 md:p-8' : ''
}`}
>

File diff suppressed because one or more lines are too long