Compare commits
4 Commits
f7c07e989d
...
d396758d4e
Author | SHA1 | Date |
---|---|---|
saml33 | d396758d4e | |
saml33 | 9ac0692349 | |
saml33 | 089daf00f3 | |
saml33 | 19ead30b17 |
|
@ -86,7 +86,7 @@ async function BlogPostPage({ params }: BlogPostPageProps) {
|
|||
|
||||
const ctaData = { ctaTitle, ctaDescription, ctaUrl }
|
||||
|
||||
const headerImageUrl = `/images/blog/${slug}.png`
|
||||
const headerImageUrl = `/images/blog/${slug}.webp`
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -88,7 +88,7 @@ async function LearnPostPage({ params }: LearnPostPageProps) {
|
|||
} = learnPost
|
||||
|
||||
const ctaData = { ctaTitle, ctaDescription, ctaUrl }
|
||||
const headerImageUrl = `/images/learn/${slug}.png`
|
||||
const headerImageUrl = `/images/learn/${slug}.webp`
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ const PostCard = ({
|
|||
const { author, createdAt, postDescription, postTitle, slug } = blogPost
|
||||
const customImagePath =
|
||||
type === 'blog'
|
||||
? `/images/blog/${slug}-small.png`
|
||||
: `/images/learn/${slug}-small.png`
|
||||
? `/images/blog/${slug}-small.webp`
|
||||
: `/images/learn/${slug}-small.webp`
|
||||
return (
|
||||
<div
|
||||
className="col-span-4 sm:col-span-2 lg:col-span-1 border border-th-bkg-3 rounded-xl group relative"
|
||||
|
|
|
@ -29,7 +29,7 @@ const Category = ({
|
|||
return sortTokens(tokensForCategory)
|
||||
}, [tokensForCategory])
|
||||
|
||||
const backgroundImageUrl = `/images/categories/${slug}.png`
|
||||
const backgroundImageUrl = `/images/categories/${slug}.webp`
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -84,7 +84,7 @@ const ExploreCategories = ({
|
|||
<Link href={`/explore/categories/${slug}`}>
|
||||
<div className="overflow-hidden rounded-t-xl">
|
||||
<CardImage
|
||||
customImagePath={`/images/categories/${slug}-small.png`}
|
||||
customImagePath={`/images/categories/${slug}-small.webp`}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 py-3">
|
||||
|
|
|
@ -24,8 +24,8 @@ const PageHeader = ({
|
|||
const imageSrc = backgroundImageUrl
|
||||
? backgroundImageUrl
|
||||
: theme === 'Dark'
|
||||
? '/images/new/cube-bg.png'
|
||||
: '/images/new/cube-bg-light.png'
|
||||
? '/images/new/cube-bg.webp'
|
||||
: '/images/new/cube-bg-light.webp'
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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.webp"
|
||||
alt=""
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlackSphere
|
|
@ -1,339 +1,52 @@
|
|||
'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%' },
|
||||
{ icon: 'coin-silver.png', x: '2%', y: '10%' },
|
||||
{ icon: 'cube-pink.png', x: '90%', y: '23%' },
|
||||
{ icon: 'spring-chrome.png', x: '79%', y: '25%' },
|
||||
{ icon: 'pyramid-blue.png', x: '95%', y: '80%' },
|
||||
{ icon: 'ring-white.png', x: '3%', y: '65%' },
|
||||
{ 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: 'pyramid-blue.png', x: '12%', y: '40%' },
|
||||
{ icon: 'coin-orange.png', x: '81%', y: '68%' },
|
||||
{ icon: 'coin-orange.webp', x: '10%', y: '20%' },
|
||||
{ icon: 'coin-silver.webp', x: '2%', y: '10%' },
|
||||
{ icon: 'cube-pink.webp', x: '90%', y: '23%' },
|
||||
{ icon: 'spring-chrome.webp', x: '79%', y: '25%' },
|
||||
{ icon: 'pyramid-blue.webp', x: '95%', y: '80%' },
|
||||
{ icon: 'ring-white.webp', x: '3%', y: '65%' },
|
||||
{ icon: 'ring-white.webp', x: '88%', y: '40%' },
|
||||
{ icon: 'coin-silver.webp', x: '86%', y: '48%' },
|
||||
{ icon: 'cube-pink.webp', x: '10%', y: '73%' },
|
||||
{ icon: 'spring-chrome.webp', x: '14%', y: '83%' },
|
||||
{ icon: 'pyramid-blue.webp', x: '12%', y: '40%' },
|
||||
{ icon: 'coin-orange.webp', x: '81%', y: '68%' },
|
||||
]
|
||||
|
||||
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.
|
||||
|
@ -354,74 +67,23 @@ const HomePage = ({
|
|||
<BlackSphere />
|
||||
<img
|
||||
className="w-3/4 absolute h-auto lg:-right-40 lg:top-1/2 lg:transform lg:-translate-y-1/2 xl:right-0 lg:w-full xl:w-[740px]"
|
||||
src="/images/new/trade-desktop.png"
|
||||
src="/images/new/trade-desktop.webp"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
className="sphere absolute lg:-bottom-24 xl:-bottom-40 -right-28 sm:-right-24 md:-right-14 lg:right-0 lg:left-0 xl:-left-16 w-80 h-auto"
|
||||
src="/images/new/orange-sphere.png"
|
||||
src="/images/new/orange-sphere.webp"
|
||||
alt=""
|
||||
/>
|
||||
</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>
|
||||
|
@ -528,7 +158,7 @@ const HomePage = ({
|
|||
/>
|
||||
<img
|
||||
className="shadow-lg mt-12 w-full sm:w-3/4 max-w-[800px] h-auto absolute left-1/2 -translate-x-1/2 bottom-16 md:bottom-10"
|
||||
src="/images/new/swap-desktop.png"
|
||||
src="/images/new/swap-desktop.webp"
|
||||
alt=""
|
||||
id="swap-desktop"
|
||||
/>
|
||||
|
@ -550,12 +180,12 @@ 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
|
||||
className={MOBILE_IMAGE_CLASSES}
|
||||
src="/images/new/trade-favorites.png"
|
||||
src="/images/new/trade-favorites.webp"
|
||||
/>
|
||||
<div className="core-text">
|
||||
<h2 className="mb-4">Leverage trade your favorite markets</h2>
|
||||
|
@ -599,13 +229,13 @@ const HomePage = ({
|
|||
</div>
|
||||
<img
|
||||
className={MOBILE_IMAGE_CLASSES}
|
||||
src="/images/new/unparalleled-safety.png"
|
||||
src="/images/new/unparalleled-safety.webp"
|
||||
/>
|
||||
</div>
|
||||
<div className="core-feature flex flex-col lg:flex-row lg:items-center py-12 lg:py-24">
|
||||
<img
|
||||
className={MOBILE_IMAGE_CLASSES}
|
||||
src="/images/new/token-listings.png"
|
||||
src="/images/new/token-listings.webp"
|
||||
/>
|
||||
<div className="core-text">
|
||||
<h2 className="mb-4 max-w-lg">Permissionless token listings</h2>
|
||||
|
@ -639,13 +269,13 @@ const HomePage = ({
|
|||
</div>
|
||||
<img
|
||||
className={MOBILE_IMAGE_CLASSES}
|
||||
src="/images/new/borrow.png"
|
||||
src="/images/new/borrow.webp"
|
||||
/>
|
||||
</div>
|
||||
</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,15 +283,15 @@ 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"
|
||||
src="/images/new/build-hori.png"
|
||||
src="/images/new/build-hori.webp"
|
||||
/>
|
||||
<img
|
||||
className="absolute left-0 h-40 md:h-56 w-auto"
|
||||
src="/images/new/build-vert.png"
|
||||
src="/images/new/build-vert.webp"
|
||||
id="build"
|
||||
/>
|
||||
</div>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,375 @@
|
|||
'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 }),
|
||||
staleTime: 1000 * 60 * 10,
|
||||
})
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
|
@ -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.webp')]`
|
||||
: `bg-[url('/images/new/stage-slice-light.webp')]`
|
||||
} 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.webp')]`
|
||||
: `bg-[url('/images/new/cube-bg-light.webp')]`
|
||||
} bg-repeat`}
|
||||
ref={ref}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
|
@ -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' : ''
|
||||
}`}
|
||||
>
|
||||
|
|
|
@ -2,7 +2,6 @@ const webpack = require('webpack')
|
|||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ['gsap'],
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
|
|
|
@ -37,18 +37,13 @@
|
|||
"dayjs": "^1.11.10",
|
||||
"decimal.js": "^10.4.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"gsap": "^3.12.2",
|
||||
"i18next": "^23.7.6",
|
||||
"i18next-resources-to-backend": "^1.2.0",
|
||||
"immer": "^10.0.3",
|
||||
"next": "^14.0.3",
|
||||
"next-i18next": "^15.0.0",
|
||||
"next-plausible": "^3.11.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-flip-numbers": "^3.0.8",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-slick": "^0.30.1",
|
||||
"recharts": "^2.10.0",
|
||||
"slick-carousel": "^1.8.1"
|
||||
|
|
Before Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 104 KiB |
After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 131 KiB |
After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 120 KiB |
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 84 KiB |
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 153 KiB |
After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 143 KiB |
After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 127 KiB |
After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 108 KiB |
After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 143 KiB |
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 128 KiB |
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 188 KiB |
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 174 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 122 KiB |