Merge pull request #175 from blockworks-foundation/mango-mints
Mango Mints
This commit is contained in:
commit
94ff5c9cf3
|
@ -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
|
||||||
|
}
|
|
@ -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 />
|
||||||
|
|
|
@ -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'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
|
@ -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'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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 |
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"bot": "Bot",
|
||||||
|
"connect-wallet": "Connect Wallet",
|
||||||
|
"mango": "Mango",
|
||||||
|
"seed": "Seed",
|
||||||
|
"whale": "Whale"
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue