Skip to content

Commit

Permalink
allow selection of nfts in pay project (#122)
Browse files Browse the repository at this point in the history
### Description

Allow the selection of NFTs in the pay project page


[Screen Recording 2023-10-25 at 1.27.33 pm.mov](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/pinTSPPZHK80UCzWAYgo/2c5972e2-9594-4737-807c-4e028a7b7599.mov)
  • Loading branch information
wraeth-eth authored Oct 25, 2023
1 parent 490e3db commit 154dd14
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 27 deletions.
47 changes: 28 additions & 19 deletions src/components/Project/ProjectPayPage.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,48 @@
import { useJbProject } from '@/hooks/useJbProject'
import { ChevronLeftIcon } from '@heroicons/react/24/outline'
import Link from 'next/link'
import React from 'react'
import { twMerge } from 'tailwind-merge'
import { Button } from '../ui/Button'
import { ProjectPayForm } from './components/ProjectPayForm'
import { ProjectPayRewards } from './components/ProjectPayRewards'
import { projectPayReducer } from './providers/projectPayReducer'
import { ProjectPayContext } from './providers/ProjectPayContext'

export const ProjectPayPage = () => {
const { name } = useJbProject()
const [state, dispatch] = React.useReducer(projectPayReducer, {
nftRewardIds: [],
})

return (
<div className="block md:flex">
{/* Left panel */}
<div className="mt-32 flex-1 px-4 pb-14 md:mt-10 md:px-8">
<div className="md:mx-auto md:max-w-lg">
<BackButton className="hidden md:flex" />
<div className="md:mt-12">
<h1 className="text-sm font-medium text-gray-500">
You are paying
</h1>
<h2 className="mt-4 font-heading text-2xl font-medium">{name}</h2>
<ProjectPayContext.Provider value={{ ...state, dispatch }}>
<div className="block md:flex">
{/* Left panel */}
<div className="mt-32 flex-1 px-4 pb-14 md:mt-10 md:px-8">
<div className="md:mx-auto md:max-w-lg">
<BackButton className="hidden md:flex" />
<div className="md:mt-12">
<h1 className="text-sm font-medium text-gray-500">
You are paying
</h1>
<h2 className="mt-4 font-heading text-2xl font-medium">{name}</h2>

<h3 className="mt-10 font-medium text-gray-800 md:mt-12">
Select a reward
</h3>
<h3 className="mt-10 font-medium text-gray-800 md:mt-12">
Select a reward
</h3>

<ProjectPayRewards className="mt-5" />
<ProjectPayRewards className="mt-5" />
</div>
</div>
</div>
</div>

{/* Right panel */}
<div className="flex-1 bg-gray-50 px-4 pb-24 pt-12 md:px-8 md:pt-[148px]">
<ProjectPayForm className="md:max-w-lg" />
{/* Right panel */}
<div className="flex-1 bg-gray-50 px-4 pb-24 pt-12 md:px-8 md:pt-[148px]">
<ProjectPayForm className="md:max-w-lg" />
</div>
</div>
</div>
</ProjectPayContext.Provider>
)
}

Expand Down
31 changes: 27 additions & 4 deletions src/components/Project/components/PayRewardCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,32 @@ import { formatEther } from 'juice-hooks'
import { RewardDialog } from './RewardDialog'
import { Button } from '@/components/ui/Button'
import { Separator } from '@/components/ui/Separator'
import React from 'react'
import { CheckIcon } from '@heroicons/react/24/solid'

export type PayRewardCardProps = {
className?: string
nft: JB721DelegateTierTier
isSelected?: boolean
onClick?: () => void
}

export const PayRewardCard: React.FC<PayRewardCardProps> = ({
className,
nft,
isSelected,
onClick,
}) => {
const remaining = nft.remainingQuantity.toString()
return (
<div
className={twMerge(
'flex rounded-[10px] border border-gray-200 p-3',
'flex cursor-pointer rounded-[10px] border border-gray-200 p-3 shadow-sm transition-all hover:border-gray-300 hover:shadow-md',
isSelected &&
'-m-[1px] border-2 border-bluebs-500 shadow-md hover:border-bluebs-500',
className,
)}
onClick={onClick}
>
<RewardImage
className="h-14 w-14 rounded-lg"
Expand All @@ -38,16 +47,30 @@ export const PayRewardCard: React.FC<PayRewardCardProps> = ({
</div>
<div className="flex h-5 gap-4 text-sm">
<RewardDialog nft={nft}>
<Button size="child" variant="link">
<Button className="whitespace-nowrap" size="child" variant="link">
View details
</Button>
</RewardDialog>
<Separator orientation="vertical" />
<span className="text-gray-400">{remaining} remaining</span>
<span className="whitespace-nowrap text-gray-400">
{remaining} remaining
</span>
</div>
</div>

<div className="h-4 w-4 rounded-full border border-gray-200" />
<div
className={twMerge(
'flex h-5 w-5 items-center justify-center rounded-full border',
isSelected ? 'border-bluebs-500 bg-bluebs-500' : 'border-gray-200',
)}
>
<CheckIcon
className={twMerge(
'h-4 w-4 stroke-2 text-white',
isSelected ? 'inline' : 'hidden',
)}
/>
</div>
</div>
)
}
23 changes: 20 additions & 3 deletions src/components/Project/components/ProjectPayForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import {
FormMessage,
} from '@/components/ui/Form'
import { zodResolver } from '@hookform/resolvers/zod'
import { PropsWithChildren, useCallback } from 'react'
import { PropsWithChildren, useCallback, useMemo } from 'react'
import { useForm } from 'react-hook-form'
import { twMerge } from 'tailwind-merge'
import { z } from 'zod'
import { useProjectPay } from '../providers/ProjectPayContext'
import { useJbProject } from '@/hooks/useJbProject'
import { formatEther } from 'juice-hooks'

const WEI = 1e-18

Expand All @@ -34,6 +37,9 @@ export type ProjectPayFormProps = {
export const ProjectPayForm: React.FC<ProjectPayFormProps> = ({
className,
}) => {
const { nfts } = useJbProject()
const { nftRewardIds } = useProjectPay()

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
Expand All @@ -44,12 +50,23 @@ export const ProjectPayForm: React.FC<ProjectPayFormProps> = ({
},
})

const totalNftSelectionPrice = useMemo(
() =>
nftRewardIds.reduce((acc, nftId) => {
if (!nfts) return acc
const nft = nfts.find(nft => nft.id === nftId)
if (!nft) return acc
return acc + nft.price
}, 0n),
[nftRewardIds, nfts],
)

const onSubmit = useCallback(async (values: z.infer<typeof formSchema>) => {
console.log(values)
}, [])

// TODO
const total = 0
const total = totalNftSelectionPrice

return (
<Form {...form}>
Expand Down Expand Up @@ -105,7 +122,7 @@ export const ProjectPayForm: React.FC<ProjectPayFormProps> = ({

<div className="mt-6 flex justify-between font-medium">
<div>Total to pay</div>
<div>{total} ETH</div>
<div>{formatEther(total)} ETH</div>
</div>

<Button className="mt-2 h-14 w-full" type="submit">
Expand Down
28 changes: 27 additions & 1 deletion src/components/Project/components/ProjectPayRewards.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useJbProject } from '@/hooks/useJbProject'
import { twMerge } from 'tailwind-merge'
import { PayRewardCard } from './PayRewardCard'
import { useProjectPay } from '../providers/ProjectPayContext'
import { useCallback } from 'react'

export type ProjectPayRewardsProps = {
className?: string
Expand All @@ -10,10 +12,34 @@ export const ProjectPayRewards: React.FC<ProjectPayRewardsProps> = ({
className,
}) => {
const { nfts } = useJbProject()
const { dispatch, nftRewardIds: selectedNftIds } = useProjectPay()

const isSelected = useCallback(
(nftId: bigint) => selectedNftIds.includes(nftId),
[selectedNftIds],
)

const toggleNft = useCallback(
(nftId: bigint) => {
if (isSelected(nftId)) {
dispatch({ type: 'removeNftReward', id: nftId })
return
}
dispatch({ type: 'addNftReward', id: nftId })
},
[dispatch, isSelected],
)

return (
<div className={twMerge('flex flex-col gap-3', className)}>
{nfts?.map(nft => <PayRewardCard key={nft.id} nft={nft} />)}
{nfts?.map(nft => (
<PayRewardCard
key={nft.id}
nft={nft}
isSelected={isSelected(nft.id)}
onClick={() => toggleNft(nft.id)}
/>
))}
</div>
)
}
19 changes: 19 additions & 0 deletions src/components/Project/providers/ProjectPayContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'
import {
ProjectPayAction,
ProjectPayState,
projectPayReducer,
} from './projectPayReducer'

export type ProjectPayContextType = ProjectPayState & {
dispatch: React.Dispatch<ProjectPayAction>
}

export const ProjectPayContext = React.createContext<ProjectPayContextType>({
nftRewardIds: [],
dispatch: () => {
console.error('ProjectPayContext not initialized')
},
})

export const useProjectPay = () => React.useContext(ProjectPayContext)
27 changes: 27 additions & 0 deletions src/components/Project/providers/projectPayReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type ProjectPayAction =
| { type: 'addNftReward'; id: bigint }
| { type: 'removeNftReward'; id: bigint }

export type ProjectPayState = {
nftRewardIds: bigint[]
}

export const projectPayReducer = (
state: ProjectPayState,
action: ProjectPayAction,
): ProjectPayState => {
switch (action.type) {
case 'addNftReward':
return {
...state,
nftRewardIds: [...state.nftRewardIds, action.id],
}
case 'removeNftReward':
return {
...state,
nftRewardIds: state.nftRewardIds.filter(id => id !== action.id),
}
default:
return state
}
}

0 comments on commit 154dd14

Please sign in to comment.