Skip to content

Commit

Permalink
feature(airdrops): Airdrops index (#15547)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xTxbi authored Feb 20, 2025
1 parent 350931c commit bec2c25
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 1 deletion.
10 changes: 9 additions & 1 deletion airdrops/app/page.tsx
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>
)
}
191 changes: 191 additions & 0 deletions airdrops/components/Campaigns.tsx
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>
)
}
40 changes: 40 additions & 0 deletions airdrops/components/Hero.tsx
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>
)
}
47 changes: 47 additions & 0 deletions airdrops/src/airdrops.json
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"
}
]
20 changes: 20 additions & 0 deletions airdrops/src/utils/eligibility.ts
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
}

0 comments on commit bec2c25

Please sign in to comment.