Merge pull request #175 from blockworks-foundation/mango-mints

Mango Mints
This commit is contained in:
saml33 2023-07-17 08:50:01 +10:00 committed by GitHub
commit 94ff5c9cf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1154 additions and 7 deletions

14
apis/whitelist.ts Normal file
View File

@ -0,0 +1,14 @@
import { WHITE_LIST_API } from 'utils/constants'
export type WhiteListedResp = {
found: boolean
}
export const fetchIsWhiteListed = async (wallet: string) => {
const data = await fetch(`${WHITE_LIST_API}isWhiteListed?wallet=${wallet}`)
const body = await data.json()
if (body.error) {
throw { error: body.error, status: data.status }
}
return body.found
}

View File

@ -25,6 +25,8 @@ import { Transition } from '@headlessui/react'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import TermsOfUseModal from './modals/TermsOfUseModal' import TermsOfUseModal from './modals/TermsOfUseModal'
import { ttCommons, ttCommonsExpanded, ttCommonsMono } from 'utils/fonts' import { ttCommons, ttCommonsExpanded, ttCommonsMono } from 'utils/fonts'
import PromoBanner from './rewards/PromoBanner'
import { useRouter } from 'next/router'
export const sideBarAnimationDuration = 300 export const sideBarAnimationDuration = 300
const termsLastUpdated = 1679441610978 const termsLastUpdated = 1679441610978
@ -41,6 +43,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
'' ''
) )
const { width } = useViewport() const { width } = useViewport()
const { asPath } = useRouter()
useEffect(() => { useEffect(() => {
if (width < breakpoints.xl) { if (width < breakpoints.xl) {
@ -117,6 +120,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
}`} }`}
> >
<TopBar /> <TopBar />
{asPath !== '/rewards' ? <PromoBanner /> : null}
{children} {children}
</div> </div>
<DeployRefreshManager /> <DeployRefreshManager />

View File

@ -8,6 +8,7 @@ interface SelectProps {
children: ReactNode children: ReactNode
className?: string className?: string
dropdownPanelClassName?: string dropdownPanelClassName?: string
icon?: ReactNode
placeholder?: string placeholder?: string
disabled?: boolean disabled?: boolean
} }
@ -18,6 +19,7 @@ const Select = ({
children, children,
className, className,
dropdownPanelClassName, dropdownPanelClassName,
icon,
placeholder = 'Select', placeholder = 'Select',
disabled = false, disabled = false,
}: SelectProps) => { }: SelectProps) => {
@ -32,13 +34,16 @@ const Select = ({
<div <div
className={`flex items-center justify-between space-x-2 px-3 text-th-fgd-1`} className={`flex items-center justify-between space-x-2 px-3 text-th-fgd-1`}
> >
{value ? ( <div className="flex items-center">
value {icon ? icon : null}
) : ( {value ? (
<span className="text-th-fgd-3">{placeholder}</span> value
)} ) : (
<span className="text-th-fgd-3">{placeholder}</span>
)}
</div>
<ChevronDownIcon <ChevronDownIcon
className={`h-5 w-5 flex-shrink-0 text-th-fgd-3 ${ className={`ml-1 h-5 w-5 flex-shrink-0 text-th-fgd-3 ${
open ? 'rotate-180' : 'rotate-360' open ? 'rotate-180' : 'rotate-360'
}`} }`}
/> />

View File

@ -0,0 +1,14 @@
const AcornIcon = ({ className }: { className?: string }) => {
return (
<svg
className={`${className}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 40 40"
fill="currentColor"
>
<path d="M31.9764 20.7091L31.47 16.9109C31.2226 15.0512 30.4484 13.36 29.3079 11.9846L31.6864 9.60615C32.034 9.2594 32.034 8.69894 31.6864 8.35219C31.3397 8.00544 30.7801 8.00544 30.4324 8.35219L28.0584 10.7262C26.674 9.56713 24.9686 8.7823 23.0903 8.53044L19.292 8.02407C18.6145 7.93273 17.9192 8.10122 17.3445 8.53221L16.5934 9.09623C16.1624 9.41814 16.1154 10.0496 16.4985 10.4327L29.5695 23.5035C29.9722 23.9061 30.5983 23.8183 30.906 23.4095L31.4691 22.6574C31.8886 22.0961 32.0686 21.4035 31.9764 20.7091ZM15.2427 11.6857L15.2108 11.6529L12.161 13.7875C9.21582 15.8493 7.64701 19.3478 8.06737 22.919L8.77062 28.8989C8.91518 30.1271 9.87385 31.0857 11.1021 31.2303L17.082 31.9335C20.6515 32.3503 24.1519 30.7851 26.2138 27.84L28.3484 24.7902L28.3156 24.7583L15.2427 11.6857Z" />
</svg>
)
}
export default AcornIcon

View File

@ -0,0 +1,14 @@
const MangoIcon = ({ className }: { className?: string }) => {
return (
<svg
className={`${className}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 40 40"
fill="currentColor"
>
<path d="M30.9524 16.6647C29.6789 12.8631 26.0028 10.0966 22.9101 9.51321C23.0607 7.74416 23.7758 6.92237 24.3655 6.55225C24.7544 6.30132 24.8799 5.79319 24.6352 5.39798C24.3906 5.00904 23.8699 4.8773 23.4872 5.12823C22.4835 5.79319 22.0883 6.9851 21.9566 8.13938C20.2628 8.08919 16.2668 8.30875 13.7575 11.3764C10.7087 15.1027 11.5367 16.2318 9.11528 19.2869C8.83298 19.6445 9.10273 20.1777 9.56068 20.1463C11.4426 20.0271 15.1564 19.4814 17.9982 17.1101C19.9805 15.4665 21.1724 13.2458 21.8625 11.4328L22.7595 12.562C21.9817 14.3812 20.7396 16.4639 18.8011 18.0762C17.1387 19.4625 15.2317 20.2781 13.494 20.7548C13.8076 21.7836 14.228 23.1073 14.3095 23.4272C14.9494 25.9491 15.5391 28.5775 14.9682 31.0555C14.4914 33.1068 16.6431 34.5747 18.5314 34.8884C21.0407 35.3087 23.6754 34.512 25.7268 33.0002C27.7844 31.4695 29.2711 29.3366 30.2686 26.9653C31.6361 23.7283 32.0627 20.002 30.9524 16.6647ZM27.9601 25.9867C26.649 29.1045 24.3844 31.4256 21.9064 32.2035C21.8437 32.2223 21.781 32.2348 21.7182 32.2348C21.4485 32.2348 21.2038 32.0592 21.1223 31.7957C21.0156 31.4632 21.1976 31.1119 21.53 31.0053C23.6629 30.3403 25.6327 28.2827 26.7996 25.5037C26.9376 25.1837 27.3014 25.0332 27.6213 25.1649C27.9412 25.3029 28.0918 25.6667 27.9601 25.9867Z" />
</svg>
)
}
export default MangoIcon

View File

@ -0,0 +1,21 @@
const RobotIcon = ({ className }: { className?: string }) => {
return (
<svg
className={`${className}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 40 40"
fill="currentColor"
>
<path d="M15.102 17.2186C14.3807 17.2186 13.7959 17.8104 13.7959 18.5404C13.7959 19.2705 14.3807 19.8623 15.102 19.8623C15.8233 19.8623 16.4082 19.2705 16.4082 18.5404C16.4082 17.8104 15.8233 17.2186 15.102 17.2186Z" />
<path d="M23.5918 18.5404C23.5918 17.8104 24.1767 17.2186 24.898 17.2186C25.6193 17.2186 26.2041 17.8104 26.2041 18.5404C26.2041 19.2705 25.6193 19.8623 24.898 19.8623C24.1767 19.8623 23.5918 19.2705 23.5918 18.5404Z" />
<path d="M24.2449 25.8107H15.7551V27.1325H24.2449V25.8107Z" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16.4082 7.30465C16.4082 5.47955 17.8701 4 19.6735 4C21.4769 4 22.9388 5.47955 22.9388 7.30465C22.9388 8.78436 21.9778 10.0369 20.6531 10.458V11.6007H30.449C31.5309 11.6007 32.4082 12.4884 32.4082 13.5835V16.5577H34.3673C35.269 16.5577 36 17.2974 36 18.21V24.8193C36 25.7318 35.269 26.4716 34.3673 26.4716H32.4082V29.4458C32.4082 30.5409 31.5309 31.4286 30.449 31.4286H9.55102C8.46907 31.4286 7.59184 30.5409 7.59184 29.4458V26.4716H5.63265C4.73103 26.4716 4 25.7318 4 24.8193V18.21C4 17.2974 4.73103 16.5577 5.63265 16.5577H7.59184V13.5835C7.59184 12.4884 8.46907 11.6007 9.55102 11.6007H18.6939V10.458C17.3691 10.0369 16.4082 8.78436 16.4082 7.30465ZM19.6735 5.98279C18.9522 5.98279 18.3673 6.57462 18.3673 7.30465C18.3673 8.03468 18.9522 8.62651 19.6735 8.62651C20.3948 8.62651 20.9796 8.03468 20.9796 7.30465C20.9796 6.57462 20.3948 5.98279 19.6735 5.98279ZM7.59184 18.5404H5.95918V24.4888H7.59184V18.5404ZM32.4082 24.4888H34.0408V18.5404H32.4082V24.4888ZM11.8367 18.5404C11.8367 16.7153 13.2986 15.2358 15.102 15.2358C16.9055 15.2358 18.3673 16.7153 18.3673 18.5404C18.3673 20.3655 16.9055 21.8451 15.102 21.8451C13.2986 21.8451 11.8367 20.3655 11.8367 18.5404ZM24.898 15.2358C23.0945 15.2358 21.6327 16.7153 21.6327 18.5404C21.6327 20.3655 23.0945 21.8451 24.898 21.8451C26.7014 21.8451 28.1633 20.3655 28.1633 18.5404C28.1633 16.7153 26.7014 15.2358 24.898 15.2358ZM13.7959 25.4802C13.7959 24.5676 14.5269 23.8279 15.4286 23.8279H24.5714C25.4731 23.8279 26.2041 24.5676 26.2041 25.4802V27.463C26.2041 28.3756 25.4731 29.1153 24.5714 29.1153H15.4286C14.5269 29.1153 13.7959 28.3756 13.7959 27.463V25.4802Z"
/>
</svg>
)
}
export default RobotIcon

View File

@ -0,0 +1,20 @@
const WhaleIcon = ({ className }: { className?: string }) => {
return (
<svg
className={`${className}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 40 40"
fill="currentColor"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M37.4952 15.4842C34.6812 14.5986 31.5602 14.4129 27.1108 16.8341C24.0041 18.5267 20.1331 19.6908 16.605 18.8552C13.084 18.0196 10.3772 14.97 9.30594 11.1776C11.27 10.1778 12.1127 5.98546 10.3987 4C7.98469 4.78561 6.21349 6.20686 6.00637 8.18518C3.56383 6.82821 1.06415 7.57811 0 8.6994C2.24257 13.4059 6.3349 12.4132 6.3349 12.4132C6.3349 12.4132 5.98494 16.5127 6.73485 21.7834C6.7577 21.9605 6.79884 22.1376 6.83997 22.3148C6.85026 22.359 6.86054 22.4033 6.87054 22.4476C6.9991 22.4119 7.1348 22.3905 7.27049 22.3905C7.65616 22.3905 8.03468 22.5476 8.31322 22.8404C8.82029 23.3832 11.6199 26.1614 16.6621 27.6541C17.5335 27.9112 18.3762 28.1112 19.2047 28.2754C18.4405 31.6393 19.4046 35.8673 19.4046 35.8673C22.0043 35.3674 25.0325 32.9248 25.8467 29.3539C25.8661 29.2513 25.8844 29.1508 25.9025 29.0517C25.931 28.8955 25.9589 28.7426 25.9895 28.5897C28.0535 28.2754 29.8676 27.547 31.4888 26.3685C31.8745 26.09 32.2244 25.8258 32.5529 25.5758C34.4527 24.1403 35.9597 23.0047 38.7164 23.0047C38.8521 23.0047 38.995 23.0047 39.1378 23.0118C39.2664 23.0118 39.3878 23.0404 39.5092 23.0761C40.4019 19.7623 40.2662 16.277 37.4952 15.4771V15.4842ZM31.878 20.2015C31.0924 20.2015 30.4497 19.5587 30.4497 18.7731C30.4497 17.9875 31.0924 17.3447 31.878 17.3447C32.6637 17.3447 33.3064 17.9875 33.3064 18.7731C33.3064 19.5587 32.6637 20.2015 31.878 20.2015Z"
/>
<path d="M16.2621 29.0254C10.7628 27.397 7.72034 24.2903 7.27754 23.8189C8.7345 27.5827 12.6483 31.2679 17.5476 32.7892C17.4762 31.7464 17.4619 30.5609 17.5905 29.3825L17.4391 29.3428C17.0525 29.2416 16.6608 29.1391 16.2692 29.0254H16.2621Z" />
<path d="M27.2036 29.8109C29.0319 29.411 30.7531 28.6754 32.3315 27.5255C32.7224 27.2405 33.0802 26.9702 33.4156 26.7169C35.3661 25.2436 36.5588 24.3427 39.0878 24.4402C38.5236 26.0257 37.7594 27.4755 36.9952 28.4397C34.167 31.9964 30.4889 33.439 25.3538 33.5962C26.1894 32.5249 26.8465 31.2536 27.2036 29.8109Z" />
</svg>
)
}
export default WhaleIcon

View File

@ -0,0 +1,175 @@
import MedalIcon from '@components/icons/MedalIcon'
import ProfileImage from '@components/profile/ProfileImage'
import { ArrowLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import {
Badge,
RewardsLeaderboardItem,
fetchLeaderboard,
tiers,
} from './RewardsPage'
import { useState } from 'react'
import Select from '@components/forms/Select'
import { IconButton } from '@components/shared/Button'
import AcornIcon from '@components/icons/AcornIcon'
import WhaleIcon from '@components/icons/WhaleIcon'
import RobotIcon from '@components/icons/RobotIcon'
import MangoIcon from '@components/icons/MangoIcon'
import { useQuery } from '@tanstack/react-query'
import SheenLoader from '@components/shared/SheenLoader'
import { abbreviateAddress } from 'utils/formatting'
import { PublicKey } from '@solana/web3.js'
import { formatNumericValue } from 'utils/numbers'
import { useTranslation } from 'next-i18next'
const Leaderboards = ({
goBack,
leaderboard,
}: {
goBack: () => void
leaderboard: string
}) => {
const { t } = useTranslation('rewards')
const [topAccountsTier, setTopAccountsTier] = useState<string>(leaderboard)
const renderTierIcon = (tier: string) => {
if (tier === 'bot') {
return <RobotIcon className="mr-2 h-5 w-5" />
} else if (tier === 'mango') {
return <MangoIcon className="mr-2 h-5 w-5" />
} else if (tier === 'whale') {
return <WhaleIcon className="mr-2 h-5 w-5" />
} else return <AcornIcon className="mr-2 h-5 w-5" />
}
const {
data: rewardsLeaderboardData,
isFetching: fetchingRewardsLeaderboardData,
isLoading: loadingRewardsLeaderboardData,
} = useQuery(
['rewards-leaderboard-data', topAccountsTier],
() => fetchLeaderboard(topAccountsTier),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
}
)
const isLoading =
fetchingRewardsLeaderboardData || loadingRewardsLeaderboardData
return (
<div className="mx-auto max-w-[1140px] flex-col items-center p-8 lg:p-10">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center">
<IconButton className="mr-2" hideBg onClick={goBack} size="small">
<ArrowLeftIcon className="h-5 w-5" />
</IconButton>
<h2 className="mr-4">Leaderboard</h2>
<Badge
label="Season 1"
borderColor="var(--active)"
shadowColor="var(--active)"
/>
</div>
<Select
className="w-32"
icon={renderTierIcon(topAccountsTier)}
value={t(topAccountsTier)}
onChange={(tier) => setTopAccountsTier(tier)}
>
{tiers.map((tier) => (
<Select.Option key={tier} value={tier}>
<div className="flex w-full items-center">
{renderTierIcon(tier)}
{t(tier)}
</div>
</Select.Option>
))}
</Select>
</div>
<div className="space-y-2">
{!isLoading ? (
rewardsLeaderboardData && rewardsLeaderboardData.length ? (
rewardsLeaderboardData.map(
(wallet: RewardsLeaderboardItem, i: number) => (
<LeaderboardCard rank={i + 1} key={i} wallet={wallet} />
)
)
) : (
<div className="flex justify-center rounded-lg border border-th-bkg-3 p-8">
<span className="text-th-fgd-3">Leaderboard not available</span>
</div>
)
) : (
<div className="space-y-2">
{[...Array(20)].map((x, i) => (
<SheenLoader className="flex flex-1" key={i}>
<div className="h-16 w-full bg-th-bkg-2" />
</SheenLoader>
))}
</div>
)}
</div>
</div>
)
}
export default Leaderboards
const LeaderboardCard = ({
rank,
wallet,
}: {
rank: number
wallet: RewardsLeaderboardItem
}) => {
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
return (
<a
className="flex w-full items-center justify-between rounded-md border border-th-bkg-3 px-3 py-3 md:px-4 md:hover:bg-th-bkg-2"
href={`/?address=${'account'}`}
rel="noopener noreferrer"
target="_blank"
>
<div className="flex items-center space-x-3">
<div
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full ${
rank < 4 ? '' : 'bg-th-bkg-3'
} md:mr-2`}
>
<p
className={`relative z-10 font-bold ${
rank < 4 ? 'text-th-bkg-1' : 'text-th-fgd-3'
}`}
>
{rank}
</p>
{rank < 4 ? <MedalIcon className="absolute" rank={rank} /> : null}
</div>
<ProfileImage
imageSize={isMobile ? '32' : '40'}
imageUrl={''}
placeholderSize={isMobile ? '20' : '24'}
/>
<div className="text-left">
<p className="capitalize text-th-fgd-2 md:text-base">
{abbreviateAddress(new PublicKey(wallet.wallet_pk))}
</p>
{/* <p className="text-xs text-th-fgd-4">
Acc: {'A1at5'.slice(0, 4) + '...' + 'tt45eU'.slice(-4)}
</p> */}
</div>
</div>
<div className="flex items-center">
<span className="mr-3 text-right font-mono md:text-base">
{formatNumericValue(wallet.points)}
</span>
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
</div>
</a>
)
}

View File

@ -0,0 +1,35 @@
import { IconButton } from '@components/shared/Button'
import { XMarkIcon } from '@heroicons/react/20/solid'
import { useIsWhiteListed } from 'hooks/useIsWhiteListed'
import Link from 'next/link'
import { useState } from 'react'
const PromoBanner = () => {
const [showBanner, setShowBanner] = useState(true)
const { data: isWhiteListed } = useIsWhiteListed()
return isWhiteListed && showBanner ? (
<div className="relative">
<div className="flex flex-wrap items-center justify-center bg-th-bkg-2 py-3 px-10">
<p className="mr-2 text-center text-th-fgd-1 text-th-fgd-1 lg:text-base">
Season 1 of Mango Mints is starting soon.
</p>
<Link
className="bg-gradient-to-b from-mango-classic-theme-active to-mango-classic-theme-down bg-clip-text font-bold text-transparent lg:text-base"
href="/rewards"
>
Get Ready
</Link>
</div>
<IconButton
className="absolute right-0 top-1/2 -translate-y-1/2 sm:right-2"
hideBg
onClick={() => setShowBanner(false)}
size="medium"
>
<XMarkIcon className="h-5 w-5 text-th-fgd-3" />
</IconButton>
</div>
) : null
}
export default PromoBanner

View File

@ -0,0 +1,786 @@
import Select from '@components/forms/Select'
import AcornIcon from '@components/icons/AcornIcon'
import MangoIcon from '@components/icons/MangoIcon'
import RobotIcon from '@components/icons/RobotIcon'
import WhaleIcon from '@components/icons/WhaleIcon'
import Button, { LinkButton } from '@components/shared/Button'
import Modal from '@components/shared/Modal'
import { Disclosure } from '@headlessui/react'
import {
ChevronDownIcon,
ChevronRightIcon,
ClockIcon,
} from '@heroicons/react/20/solid'
// import { useTranslation } from 'next-i18next'
import Image from 'next/image'
import { ReactNode, RefObject, useEffect, useRef, useState } from 'react'
import Particles from 'react-tsparticles'
import { ModalProps } from 'types/modal'
import Leaderboards from './Leaderboards'
import { useQuery } from '@tanstack/react-query'
import { useWallet } from '@solana/wallet-adapter-react'
import { MANGO_DATA_API_URL } from 'utils/constants'
import { formatNumericValue } from 'utils/numbers'
import SheenLoader from '@components/shared/SheenLoader'
import { abbreviateAddress } from 'utils/formatting'
import { PublicKey } from '@solana/web3.js'
import { useTranslation } from 'next-i18next'
import { useIsWhiteListed } from 'hooks/useIsWhiteListed'
import InlineNotification from '@components/shared/InlineNotification'
const FAQS = [
{
q: 'What is Mango Mints?',
a: 'Mango Mints is a weekly rewards program with amazing prizes. Anyone can participate simply by performing actions on Mango.',
},
{
q: 'How do I participate?',
a: "Simply by using Mango. Points are allocated for transactions across the platform (swaps, trades, orders and more). You'll receive a notificaton when you earn points (make sure notifications are enabled for your wallet).",
},
{
q: 'How do Seasons work?',
a: 'Each weekly cycle is called a Season and each Season has two periods. The first period is about earning points and runs from midnight Sunday UTC to midnight Friday UTC. The second period is allocated to claim prizes and runs from midnight Friday UTC to midnight Sunday UTC.',
},
{
q: 'What are the rewards tiers?',
a: "There are 4 rewards tiers. Everyone starts in the Seed tier. After your first Season is completed you'll be promoted to either the Mango or Whale tier (depending on the average notional value of your swaps/trades). Bots are automatically assigned to the Bots tier and will remain there.",
},
{
q: 'How do the prizes work?',
a: "At the end of each Season loot boxes are distributed based on the amount of points earned relative to the other participants in your tier. Each box contains a prize. So you're guaranteed to get something.",
},
{
q: 'What happens during the Season claim period?',
a: "During the claim period you can come back to this page and often as you like and open your loot boxes. However, if you don't claim your prizes during this time window they will be lost.",
},
]
export type RewardsLeaderboardItem = {
points: number
tier: string
wallet_pk: string
}
export const tiers = ['seed', 'mango', 'whale', 'bot']
const fetchRewardsPoints = async (walletPk: string | undefined) => {
try {
const data = await fetch(
`${MANGO_DATA_API_URL}/user-data/campaign-total-points-wallet?wallet-pk=${walletPk}`
)
const res = await data.json()
return res
} catch (e) {
console.log('Failed to fetch points', e)
}
}
export const fetchLeaderboard = async (tier: string | undefined) => {
try {
const data = await fetch(
`${MANGO_DATA_API_URL}/user-data/campaign-leaderboard?tier=${tier}`
)
const res = await data.json()
return res
} catch (e) {
console.log('Failed to top accounts leaderboard', e)
}
}
const RewardsPage = () => {
// const { t } = useTranslation(['common', 'rewards'])
const [showClaim] = useState(true)
const { data: isWhiteListed, isLoading, isFetching } = useIsWhiteListed()
const [showLeaderboards, setShowLeaderboards] = useState('')
const [showWhitelistModal, setShowWhitelistModal] = useState(false)
const faqRef = useRef<HTMLDivElement>(null)
const scrollToFaqs = () => {
if (faqRef.current) {
faqRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start', // or 'end' or 'center'
})
}
}
useEffect(() => {
if (!isWhiteListed && !isLoading && !isFetching) {
setShowWhitelistModal(true)
}
}, [isWhiteListed, isLoading, isFetching])
return !showLeaderboards ? (
<>
<div className="bg-[url('/images/rewards/madlad-tile.png')]">
<div className="mx-auto flex max-w-[1140px] flex-col items-center p-8 lg:flex-row lg:p-10">
<div className="mb-6 h-[180px] w-[180px] flex-shrink-0 lg:mr-10 lg:mb-0 lg:h-[220px] lg:w-[220px]">
<Image
className="rounded-lg shadow-lg"
priority
src="/images/rewards/madlad.png"
width={260}
height={260}
alt="Top Prize"
/>
</div>
<div className="flex flex-col items-center lg:items-start">
<Badge
label="Season 1"
borderColor="var(--active)"
shadowColor="var(--active)"
/>
<h1 className="my-2 text-center text-4xl lg:text-left">
Win amazing prizes every week.
</h1>
<p className="mb-4 text-center text-lg leading-snug lg:text-left">
Earn points by performing actions on Mango. More points equals
more chances to win.
</p>
<Button size="large" onClick={scrollToFaqs}>
How it Works
</Button>
</div>
</div>
</div>
{!showClaim ? (
<Claim />
) : (
<Season
faqRef={faqRef}
showLeaderboard={setShowLeaderboards}
setShowWhitelistModal={() => setShowWhitelistModal(true)}
/>
)}
{showWhitelistModal ? (
<WhitelistWalletModal
isOpen={showWhitelistModal}
onClose={() => setShowWhitelistModal(false)}
/>
) : null}
</>
) : (
<Leaderboards
leaderboard={showLeaderboards}
goBack={() => setShowLeaderboards('')}
/>
)
}
export default RewardsPage
const Season = ({
faqRef,
showLeaderboard,
setShowWhitelistModal,
}: {
faqRef: RefObject<HTMLDivElement>
showLeaderboard: (x: string) => void
setShowWhitelistModal: () => void
}) => {
const { t } = useTranslation(['common', 'rewards'])
const { wallet } = useWallet()
const [topAccountsTier, setTopAccountsTier] = useState('seed')
const { data: isWhiteListed } = useIsWhiteListed()
const {
data: walletRewardsData,
isFetching: fetchingWalletRewardsData,
isLoading: loadingWalletRewardsData,
} = useQuery(
['rewards-points', wallet?.adapter.publicKey],
() => fetchRewardsPoints(wallet?.adapter.publicKey?.toString()),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!wallet?.adapter,
}
)
const {
data: topAccountsLeaderboardData,
isFetching: fetchingTopAccountsLeaderboardData,
isLoading: loadingTopAccountsLeaderboardData,
} = useQuery(
['top-accounts-leaderboard-data', topAccountsTier],
() => fetchLeaderboard(topAccountsTier),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
}
)
useEffect(() => {
if (walletRewardsData?.tier) {
setTopAccountsTier(walletRewardsData.tier)
}
}, [walletRewardsData])
const isLoadingWalletData =
fetchingWalletRewardsData || loadingWalletRewardsData
const isLoadingLeaderboardData =
fetchingTopAccountsLeaderboardData || loadingTopAccountsLeaderboardData
return (
<>
<div className="flex items-center justify-center bg-th-bkg-3 px-4 py-3">
<ClockIcon className="mr-2 h-5 w-5 text-th-active" />
<p className="text-base text-th-fgd-2">
Season 1 starts in:{' '}
<span className="mr-4 font-bold text-th-fgd-1">4 days</span>
</p>
</div>
<div className="mx-auto grid max-w-[1140px] grid-cols-12 gap-4 p-8 lg:gap-6 lg:p-10">
{!isWhiteListed ? (
<div className="col-span-12">
<InlineNotification
desc={
<>
<span>
You need to whitelist your wallet to claim any rewards you
win
</span>
<LinkButton className="mt-2" onClick={setShowWhitelistModal}>
Get Whitelisted
</LinkButton>
</>
}
title="Wallet not whitelisted"
type="warning"
/>
</div>
) : null}
<div className="col-span-12 lg:col-span-8">
<div className="mb-2 rounded-lg border border-th-bkg-3 p-4">
<h2 className="mb-4">Rewards Tiers</h2>
<div className="mb-6 space-y-2">
<RewardsTierCard
icon={<AcornIcon className="h-8 w-8 text-th-fgd-2" />}
name="seed"
desc="All new participants start here"
showLeaderboard={showLeaderboard}
status={walletRewardsData?.tier === 'seed' ? 'Qualified' : ''}
/>
<RewardsTierCard
icon={<MangoIcon className="h-8 w-8 text-th-fgd-2" />}
name="mango"
desc="Average swap/trade value less than $1,000"
showLeaderboard={showLeaderboard}
status={walletRewardsData?.tier === 'mango' ? 'Qualified' : ''}
/>
<RewardsTierCard
icon={<WhaleIcon className="h-8 w-8 text-th-fgd-2" />}
name="whale"
desc="Average swap/trade value greater than $1,000"
showLeaderboard={showLeaderboard}
status={walletRewardsData?.tier === 'whale' ? 'Qualified' : ''}
/>
<RewardsTierCard
icon={<RobotIcon className="h-8 w-8 text-th-fgd-2" />}
name="bot"
desc="All bots"
showLeaderboard={showLeaderboard}
status={walletRewardsData?.tier === 'bot' ? 'Qualified' : ''}
/>
</div>
</div>
<div ref={faqRef}>
<Faqs />
</div>
</div>
<div className="col-span-12 lg:col-span-4">
<div className="mb-2 rounded-lg border border-th-bkg-3 p-4">
<div className="mb-4 flex items-center justify-between">
<h2>Your Points</h2>
{isWhiteListed ? (
<Badge
label="Whitelisted"
borderColor="var(--success)"
shadowColor="var(--success)"
/>
) : null}
</div>
<div className="mb-4 flex h-14 w-full items-center rounded-md bg-th-bkg-2 px-3">
<span className="w-full font-display text-3xl text-th-fgd-1">
{!isLoadingWalletData ? (
walletRewardsData?.points ? (
formatNumericValue(walletRewardsData.points)
) : wallet?.adapter.publicKey ? (
0
) : (
<span className="flex items-center justify-center text-center font-body text-sm text-th-fgd-3">
{t('connect-wallet')}
</span>
)
) : (
<SheenLoader>
<div className="h-8 w-32 rounded-md bg-th-bkg-3" />
</SheenLoader>
)}
</span>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<p>Points Earned</p>
<p className="font-mono text-th-fgd-2">
{!isLoadingWalletData ? (
walletRewardsData?.points ? (
formatNumericValue(walletRewardsData.points)
) : wallet?.adapter.publicKey ? (
0
) : (
''
)
) : (
<SheenLoader>
<div className="h-4 w-12 rounded-sm bg-th-bkg-3" />
</SheenLoader>
)}
</p>
</div>
<div className="flex justify-between">
<p>Streak Bonus</p>
<p className="font-mono text-th-fgd-2">0x</p>
</div>
<div className="flex justify-between">
<p>Rewards Tier</p>
<p className="text-th-fgd-2">
{!isLoadingWalletData ? (
walletRewardsData?.tier ? (
<span className="capitalize">
{walletRewardsData.tier}
</span>
) : (
''
)
) : (
<SheenLoader>
<div className="h-4 w-12 rounded-sm bg-th-bkg-3" />
</SheenLoader>
)}
</p>
</div>
<div className="flex justify-between">
<p>Rank</p>
<p className="text-th-fgd-2"></p>
</div>
</div>
</div>
<div className="rounded-lg border border-th-bkg-3 p-4">
<div className="mb-4 flex items-center justify-between">
<h2 className="">Top Accounts</h2>
<Select
value={t(`rewards:${topAccountsTier}`)}
onChange={(tier) => setTopAccountsTier(tier)}
>
{tiers.map((tier) => (
<Select.Option key={tier} value={tier}>
<div className="flex w-full items-center justify-between">
{t(`rewards:${tier}`)}
</div>
</Select.Option>
))}
</Select>
</div>
<div className="border-b border-th-bkg-3">
{!isLoadingLeaderboardData ? (
topAccountsLeaderboardData &&
topAccountsLeaderboardData.length ? (
topAccountsLeaderboardData
.slice(0, 5)
.map((wallet: RewardsLeaderboardItem, i: number) => (
<div
className="flex items-center justify-between border-t border-th-bkg-3 p-3"
key={i + wallet.wallet_pk}
>
<div className="flex items-center space-x-2 font-mono">
<span>{i + 1}.</span>
<span className="text-th-fgd-3">
{abbreviateAddress(new PublicKey(wallet.wallet_pk))}
</span>
</div>
<span className="font-mono text-th-fgd-1">
{formatNumericValue(wallet.points, 0)}
</span>
</div>
))
) : (
<div className="flex justify-center border-t border-th-bkg-3 py-4">
<span className="text-th-fgd-3">
Leaderboard not available
</span>
</div>
)
) : (
<div className="space-y-0.5">
{[...Array(5)].map((x, i) => (
<SheenLoader className="flex flex-1" key={i}>
<div className="h-10 w-full bg-th-bkg-2" />
</SheenLoader>
))}
</div>
)}
</div>
<Button
className="mt-6 w-full"
onClick={() => showLeaderboard(topAccountsTier)}
secondary
>
Full Leaderboard
</Button>
</div>
</div>
</div>
</>
)
}
const Claim = () => {
const [showWinModal, setShowWinModal] = useState(false)
const [showLossModal, setShowLossModal] = useState(false)
return (
<>
<div className="flex items-center justify-center bg-th-bkg-3 px-4 py-3">
<ClockIcon className="mr-2 h-5 w-5 text-th-active" />
<p className="text-base text-th-fgd-2">
Season 1 claim ends in:{' '}
<span className="font-bold text-th-fgd-1">24 hours</span>
</p>
</div>
<div className="mx-auto grid max-w-[1140px] grid-cols-12 gap-4 p-8 lg:gap-6 lg:p-10">
<div className="col-span-12">
<div className="mb-6 text-center md:mb-12">
<h2 className="mb-2 text-5xl">Congratulations!</h2>
<p className="text-lg">You earnt 3 boxes in Season 1</p>
</div>
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:justify-center md:space-x-6 md:space-y-0">
<div className="flex w-full flex-col items-center rounded-lg border border-th-bkg-3 p-6 md:w-1/3">
<Image
className="md:-mt-10"
src="/images/rewards/cube.png"
width={140}
height={140}
alt="Reward"
style={{ width: 'auto', maxWidth: '140px' }}
/>
<Button className="mt-8" size="large">
Open Box
</Button>
</div>
<div className="flex w-full flex-col items-center rounded-lg border border-th-bkg-3 p-6 md:w-1/3">
<Image
className="md:-mt-10"
src="/images/rewards/cube.png"
width={140}
height={140}
alt="Reward"
style={{ width: 'auto', maxWidth: '140px' }}
/>
<Button
className="mt-8"
size="large"
onClick={() => setShowLossModal(true)}
>
Open Box
</Button>
</div>
<div className="flex w-full flex-col items-center rounded-lg border border-th-bkg-3 p-6 md:w-1/3">
<Image
className="md:-mt-10"
src="/images/rewards/cube.png"
width={140}
height={140}
alt="Reward"
style={{ width: 'auto', maxWidth: '140px' }}
/>
<Button
className="mt-8"
onClick={() => setShowWinModal(true)}
size="large"
>
Open Box
</Button>
</div>
</div>
</div>
</div>
{showWinModal ? (
<ClaimWinModal
isOpen={showWinModal}
onClose={() => setShowWinModal(false)}
/>
) : null}
{showLossModal ? (
<ClaimLossModal
isOpen={showLossModal}
onClose={() => setShowLossModal(false)}
/>
) : null}
</>
)
}
const RewardsTierCard = ({
desc,
icon,
name,
showLeaderboard,
status,
}: {
desc: string
icon: ReactNode
name: string
showLeaderboard: (x: string) => void
status?: string
}) => {
const { t } = useTranslation('rewards')
return (
<button
className="w-full rounded-lg bg-th-bkg-2 p-4 text-left focus:outline-none md:hover:bg-th-bkg-3"
onClick={() => showLeaderboard(name)}
>
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="mr-4 flex h-14 w-14 items-center justify-center rounded-full bg-th-bkg-1">
{icon}
</div>
<div>
<h3>{t(name)}</h3>
<p>{desc}</p>
</div>
</div>
<div className="flex items-center pl-4">
{status ? (
<Badge
label={status}
borderColor="var(--success)"
shadowColor="var(--success)"
/>
) : null}
<ChevronRightIcon className="ml-4 h-6 w-6 text-th-fgd-3" />
</div>
</div>
</button>
)
}
export const Badge = ({
label,
fillColor,
shadowColor,
borderColor,
}: {
label: string
fillColor?: string
shadowColor?: string
borderColor: string
}) => {
return (
<div
className="w-max rounded-full border px-3 py-1"
style={{
background: fillColor ? fillColor : 'transparent',
borderColor: borderColor,
boxShadow: shadowColor ? `0px 0px 8px 0px ${shadowColor}` : 'none',
}}
>
<span style={{ color: fillColor ? 'var(--fgd-1)' : borderColor }}>
{label}
</span>
</div>
)
}
const particleOptions = {
detectRetina: true,
emitters: {
life: {
count: 60,
delay: 0,
duration: 0.1,
},
startCount: 0,
particles: {
shape: {
type: ['character', 'character', 'character', 'character', 'character'],
options: {
character: [
{
fill: true,
font: 'Verdana',
value: ['🍀', '🦄', '⭐️', '🎉', '💸'],
style: '',
weight: 400,
},
],
},
},
opacity: {
value: 1,
},
rotate: {
value: {
min: 0,
max: 360,
},
direction: 'random',
animation: {
enable: true,
speed: 30,
},
},
tilt: {
direction: 'random',
enable: true,
value: {
min: 0,
max: 360,
},
animation: {
enable: true,
speed: 30,
},
},
size: {
value: 16,
},
roll: {
darken: {
enable: true,
value: 25,
},
enable: true,
speed: {
min: 5,
max: 15,
},
},
move: {
angle: 10,
attract: {
rotate: {
x: 600,
y: 1200,
},
},
direction: 'bottom',
enable: true,
speed: { min: 8, max: 16 },
outMode: 'destroy',
},
},
position: {
x: { random: true },
y: 0,
},
},
}
const ClaimWinModal = ({ isOpen, onClose }: ModalProps) => {
return (
<>
<Modal isOpen={isOpen} onClose={onClose}>
<div className="mb-6 text-center">
<h2 className="mb-6">You&apos;re a winner!</h2>
<div
className="mx-auto mb-3 h-48 w-48 rounded-lg border border-th-success"
style={{
boxShadow: '0px 0px 8px 0px var(--success)',
}}
></div>
<p className="text-lg">Prize name goes here</p>
</div>
<Button className="w-full" size="large">
Claim Prize
</Button>
</Modal>
<div className="relative z-50">
<Particles id="tsparticles" options={particleOptions} />
</div>
</>
)
}
const ClaimLossModal = ({ isOpen, onClose }: ModalProps) => {
return (
<>
<Modal isOpen={isOpen} onClose={onClose}>
<div className="mb-6 text-center">
<h2 className="mb-2">Better luck next time</h2>
<p className="text-lg">This box is empty</p>
</div>
<Button className="w-full" onClick={onClose} size="large">
Close
</Button>
</Modal>
</>
)
}
const Faqs = () => {
return (
<div className="rounded-lg border border-th-bkg-3 p-4">
<h2 className="mb-2">How it Works</h2>
<p className="mb-4">
Feel free to reach out to us on{' '}
<a
href="https://discord.gg/2uwjsBc5yw"
target="_blank"
rel="noopener noreferrer"
>
Discord
</a>{' '}
with additional questions.
</p>
<div className="border-b border-th-bkg-3">
{FAQS.map((faq, i) => (
<Disclosure key={i}>
{({ open }) => (
<>
<Disclosure.Button
className={`w-full border-t border-th-bkg-3 p-4 text-left focus:outline-none md:hover:bg-th-bkg-2`}
>
<div className="flex items-center justify-between">
<p className="text-th-fgd-2">{faq.q}</p>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} h-5 w-5 flex-shrink-0`}
/>
</div>
</Disclosure.Button>
<Disclosure.Panel className="p-4">
<p>{faq.a}</p>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
</div>
</div>
)
}
const WhitelistWalletModal = ({ isOpen, onClose }: ModalProps) => {
return (
<>
<Modal isOpen={isOpen} onClose={onClose}>
<div className="mb-6 text-center">
<h2 className="mb-2">Whitelist Wallet</h2>
<p className="text-lg">
Wallets are required to be verified with your Discord account to
participate in Mango Mints. We are doing this as a sybil prevention
mechanism.
</p>
</div>
<Button className="w-full" onClick={onClose} size="large">
Whitelist Wallet
</Button>
</Modal>
</>
)
}

View File

@ -8,6 +8,7 @@ type ModalProps = {
fullScreen?: boolean fullScreen?: boolean
isOpen: boolean isOpen: boolean
onClose: () => void onClose: () => void
panelClassNames?: string
hideClose?: boolean hideClose?: boolean
} }
@ -17,6 +18,7 @@ function Modal({
fullScreen = false, fullScreen = false,
isOpen, isOpen,
onClose, onClose,
panelClassNames,
hideClose, hideClose,
}: ModalProps) { }: ModalProps) {
const handleClose = () => { const handleClose = () => {
@ -48,7 +50,7 @@ function Modal({
fullScreen fullScreen
? '' ? ''
: 'p-4 pt-6 sm:h-auto sm:max-w-md sm:rounded-lg sm:border sm:border-th-bkg-3 sm:p-6' : 'p-4 pt-6 sm:h-auto sm:max-w-md sm:rounded-lg sm:border sm:border-th-bkg-3 sm:p-6'
} relative `} } relative ${panelClassNames}`}
> >
<div>{children}</div> <div>{children}</div>
{!hideClose ? ( {!hideClose ? (

22
hooks/useIsWhiteListed.ts Normal file
View File

@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query'
import { useWallet } from '@solana/wallet-adapter-react'
import { fetchIsWhiteListed } from 'apis/whitelist'
const refetchMs = 24 * 60 * 60 * 1000
export function useIsWhiteListed() {
const { publicKey } = useWallet()
const walletPubKey = publicKey?.toBase58()
const criteria = walletPubKey
return useQuery(
['isWhiteListed', criteria],
() => fetchIsWhiteListed(walletPubKey!),
{
enabled: !!walletPubKey,
staleTime: refetchMs,
retry: 1,
refetchInterval: refetchMs,
}
)
}

26
pages/rewards.tsx Normal file
View File

@ -0,0 +1,26 @@
import RewardsPage from '@components/rewards/RewardsPage'
import { useIsWhiteListed } from 'hooks/useIsWhiteListed'
import type { NextPage } from 'next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, [
'common',
'notifications',
'onboarding',
'profile',
'rewards',
'search',
])),
},
}
}
const Rewards: NextPage = () => {
const { data: isWhiteListed } = useIsWhiteListed()
return <div className="pb-20 md:pb-0">{isWhiteListed && <RewardsPage />}</div>
}
export default Rewards

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

View File

@ -0,0 +1,7 @@
{
"bot": "Bot",
"connect-wallet": "Connect Wallet",
"mango": "Mango",
"seed": "Seed",
"whale": "Whale"
}

View File

@ -99,6 +99,7 @@ export const JUPITER_API_DEVNET = 'https://api.jup.ag/api/tokens/devnet'
export const JUPITER_PRICE_API_MAINNET = 'https://price.jup.ag/v4/' export const JUPITER_PRICE_API_MAINNET = 'https://price.jup.ag/v4/'
export const NOTIFICATION_API = 'https://notifications-api.herokuapp.com/' export const NOTIFICATION_API = 'https://notifications-api.herokuapp.com/'
export const NOTIFICATION_API_WEBSOCKET = export const NOTIFICATION_API_WEBSOCKET =
'wss://notifications-api.herokuapp.com/ws' 'wss://notifications-api.herokuapp.com/ws'
@ -129,5 +130,6 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = {
'wbtc (portal)': true, 'wbtc (portal)': true,
} }
export const WHITE_LIST_API = 'https://api.mngo.cloud/whitelist/v1/'
export const DAILY_SECONDS = 86400 export const DAILY_SECONDS = 86400
export const DAILY_MILLISECONDS = 86400000 export const DAILY_MILLISECONDS = 86400000