-
-
Notifications
You must be signed in to change notification settings - Fork 262
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature(airdrops): Airdrops index (#15547)
- Loading branch information
Showing
5 changed files
with
307 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,11 @@ | ||
import Campaigns from '../components/Campaigns' | ||
import Hero from '../components/Hero' | ||
|
||
export default function Home() { | ||
return <div className="flex flex-col gap-10 h-full">Unlock Airdrops</div> | ||
return ( | ||
<div className="flex flex-col gap-10 h-full"> | ||
<Hero /> | ||
<Campaigns /> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
'use client' | ||
|
||
import useEmblaCarousel from 'embla-carousel-react' | ||
import { useCallback, useEffect, useState } from 'react' | ||
import { FiArrowLeft, FiArrowRight } from 'react-icons/fi' | ||
import { Container } from './layout/Container' | ||
import airDrops from '../src/airdrops.json' | ||
import { Button } from '@unlock-protocol/ui' | ||
import Link from 'next/link' | ||
import { usePrivy } from '@privy-io/react-auth' | ||
import { isEligible } from '../src/utils/eligibility' | ||
|
||
interface AirdropData { | ||
id: string | ||
title: string | ||
description: string | ||
contractAddress: string | ||
tokenAmount: string | ||
tokenSymbol: string | ||
recipientsFile: string | ||
} | ||
|
||
interface CampaignCardProps { | ||
title: string | ||
description: string | ||
contractAddress: string | ||
authenticated: boolean | ||
isEligible?: number | ||
} | ||
|
||
const CampaignCard = ({ | ||
title, | ||
description, | ||
contractAddress, | ||
isEligible, | ||
authenticated, | ||
}: CampaignCardProps) => { | ||
return ( | ||
<Link | ||
href={`#${contractAddress}`} | ||
className={`block h-full p-6 space-y-4 border min-w-[24rem] sm:min-w-[28rem] rounded-xl transition-all duration-200 ${ | ||
authenticated | ||
? isEligible > 0 | ||
? 'hover:border-brand-ui-primary' | ||
: 'opacity-50 cursor-not-allowed hover:border-gray-200' | ||
: '' | ||
}`} | ||
> | ||
<div className="space-y-4"> | ||
<h3 className="text-xl font-medium">{title}</h3> | ||
<p className="text-gray-600 line-clamp-3">{description}</p> | ||
<div className="flex items-center justify-between"> | ||
<Button disabled={!authenticated || !isEligible}> | ||
{!authenticated | ||
? 'Connect Wallet' | ||
: isEligible > 0 | ||
? 'Claim Rewards' | ||
: 'Not Eligible'} | ||
</Button> | ||
{authenticated && ( | ||
<div | ||
className={`text-sm font-medium ${ | ||
isEligible > 0 ? 'text-green-600' : 'text-gray-500' | ||
}`} | ||
> | ||
{isEligible > 0 | ||
? `Eligible for ${isEligible} UP` | ||
: 'Not Eligible'} | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
</Link> | ||
) | ||
} | ||
|
||
const CampaignsContent = () => { | ||
const [viewportRef, embla] = useEmblaCarousel({ | ||
dragFree: true, | ||
slidesToScroll: 1, | ||
containScroll: 'trimSnaps', | ||
}) | ||
|
||
const [prevBtnEnabled, setPrevBtnEnabled] = useState(false) | ||
const [nextBtnEnabled, setNextBtnEnabled] = useState(false) | ||
|
||
const scrollPrev = useCallback(() => embla && embla.scrollPrev(), [embla]) | ||
const scrollNext = useCallback(() => embla && embla.scrollNext(), [embla]) | ||
|
||
const onSelect = useCallback(() => { | ||
if (!embla) return | ||
setPrevBtnEnabled(embla.canScrollPrev()) | ||
setNextBtnEnabled(embla.canScrollNext()) | ||
}, [embla]) | ||
|
||
useEffect(() => { | ||
if (!embla) return | ||
embla.on('select', onSelect) | ||
onSelect() | ||
}, [embla, onSelect]) | ||
|
||
const { authenticated, user } = usePrivy() | ||
const [eligibility, setEligibility] = useState<Record<string, number>>({}) | ||
|
||
useEffect(() => { | ||
const checkEligibility = async () => { | ||
if (!authenticated || !user?.wallet?.address) return | ||
|
||
const eligibilityChecks = await Promise.all( | ||
(airDrops as AirdropData[]).map(async (drop) => { | ||
const amount = await isEligible( | ||
user.wallet.address, | ||
drop.recipientsFile | ||
) | ||
return [drop.contractAddress, amount] as const | ||
}) | ||
) | ||
|
||
setEligibility(Object.fromEntries(eligibilityChecks)) | ||
} | ||
|
||
checkEligibility() | ||
}, [authenticated, user?.wallet?.address]) | ||
|
||
return ( | ||
<Container> | ||
<div className="space-y-4"> | ||
<header className="flex items-center justify-between"> | ||
<div> | ||
<h2 className="text-xl font-semibold sm:text-3xl"> | ||
Ongoing Campaigns | ||
</h2> | ||
<p className="text-lg text-gray-600"> | ||
Claim your rewards from eligible campaigns. | ||
</p> | ||
</div> | ||
|
||
<div className="justify-end hidden gap-4 sm:flex"> | ||
<button | ||
className="p-2 border rounded-full disabled:opacity-25 disabled:cursor-not-allowed border-gray-300" | ||
aria-label="previous" | ||
onClick={scrollPrev} | ||
disabled={!prevBtnEnabled} | ||
> | ||
<FiArrowLeft size={24} /> | ||
</button> | ||
<button | ||
className="p-2 border rounded-full disabled:opacity-25 disabled:cursor-not-allowed border-gray-300" | ||
aria-label="next" | ||
onClick={scrollNext} | ||
disabled={!nextBtnEnabled} | ||
> | ||
<FiArrowRight size={24} /> | ||
</button> | ||
</div> | ||
</header> | ||
|
||
<div className="relative max-w-fit"> | ||
<div className="overflow-hidden cursor-move" ref={viewportRef}> | ||
<div className="flex gap-8 py-6 select-none"> | ||
{(airDrops as AirdropData[]).map((drop) => ( | ||
<CampaignCard | ||
key={drop.contractAddress} | ||
contractAddress={drop.contractAddress} | ||
title={drop.title} | ||
description={drop.description} | ||
isEligible={eligibility[drop.contractAddress] ?? 0} | ||
authenticated={authenticated} | ||
/> | ||
))} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</Container> | ||
) | ||
} | ||
|
||
export default function Campaigns() { | ||
const { authenticated } = usePrivy() | ||
|
||
if (!authenticated) { | ||
return null | ||
} | ||
|
||
return ( | ||
<div id="campaigns-section"> | ||
<CampaignsContent /> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
'use client' | ||
|
||
import { Button } from '@unlock-protocol/ui' | ||
import { Container } from './layout/Container' | ||
import { usePrivy } from '@privy-io/react-auth' | ||
|
||
export default function Hero() { | ||
const { ready, authenticated, login } = usePrivy() | ||
|
||
return ( | ||
<div className="h-[50vh] flex items-center"> | ||
<Container> | ||
<div className="flex flex-col items-center justify-center"> | ||
<div className="text-center max-w-3xl mx-auto"> | ||
<h1 className="text-5xl font-bold mb-6">Claim Your Rewards</h1> | ||
<p className="text-xl text-gray-600 mb-6"> | ||
Participate in our ecosystem rewards program and claim UP tokens. | ||
Connect your wallet to check your eligibility for various | ||
airdrops. | ||
</p> | ||
<div className="flex gap-4 justify-center"> | ||
{!authenticated && ( | ||
<Button | ||
disabled={!ready} | ||
onClick={() => { | ||
if (ready && !authenticated) { | ||
login() | ||
} | ||
}} | ||
> | ||
Connect Wallet | ||
</Button> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
</Container> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
[ | ||
{ | ||
"id": "1", | ||
"title": "Airdrop #1", | ||
"description": "A mysterious collection of digital artifacts found in an abandoned crypto wallet. Legend says they multiply when no one is watching.", | ||
"contractAddress": "0x1234567890123456789012345678901234567890", | ||
"tokenAmount": "5000", | ||
"tokenSymbol": "UP", | ||
"recipientsFile": "https://example.com/airdrop1-recipients.json" | ||
}, | ||
{ | ||
"id": "2", | ||
"title": "Airdrop #2", | ||
"description": "Tokens that smell like fresh baked cookies straight from the blockchain oven. Warning: Do not attempt to eat these digital assets, no matter how tempting they may be.", | ||
"contractAddress": "0x2345678901234567890123456789012345678901", | ||
"tokenAmount": "10000", | ||
"tokenSymbol": "UP", | ||
"recipientsFile": "https://example.com/airdrop2-recipients.json" | ||
}, | ||
{ | ||
"id": "3", | ||
"title": "Airdrop #3", | ||
"description": "A collection of tokens that were discovered floating in the metaverse. They seem to hum a catchy tune when transferred.", | ||
"contractAddress": "0x3456789012345678901234567890123456789012", | ||
"tokenAmount": "10000", | ||
"tokenSymbol": "UP", | ||
"recipientsFile": "https://example.com/airdrop3-recipients.json" | ||
}, | ||
{ | ||
"id": "4", | ||
"title": "Airdrop #4", | ||
"description": "These tokens were created during a solar eclipse while a cat walked across a keyboard. Side effects may include spontaneous dancing.", | ||
"contractAddress": "0x4567890123456789012345678901234567890123", | ||
"tokenAmount": "10000", | ||
"tokenSymbol": "UP", | ||
"recipientsFile": "https://example.com/airdrop4-recipients.json" | ||
}, | ||
{ | ||
"id": "5", | ||
"title": "Airdrop #5", | ||
"description": "Rumor has it these tokens were minted by time-traveling robots from the year 3000. They occasionally speak in riddles.", | ||
"contractAddress": "0x5678901234567890123456789012345678901234", | ||
"tokenAmount": "10000", | ||
"tokenSymbol": "UP", | ||
"recipientsFile": "https://example.com/airdrop5-recipients.json" | ||
} | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/** | ||
* Checks if an address is eligible for an airdrop and returns the token amount | ||
* This is a temporary implementation that randomly determines eligibility | ||
* To be replaced with actual implementation that checks against the recipients file | ||
*/ | ||
export const isEligible = async ( | ||
address: string, | ||
recipientsFile: string | ||
): Promise<number> => { | ||
// Temporary implementation: randomly determine eligibility | ||
const random = Math.random() | ||
|
||
// 40% chance of being eligible | ||
if (random < 0.4) { | ||
// Random amount between 100 and 1000 tokens | ||
return Math.floor(Math.random() * 900) + 100 | ||
} | ||
|
||
return 0 | ||
} |