Skip to content

Commit

Permalink
feat(cr_nft): add nft boost status card
Browse files Browse the repository at this point in the history
TOK-617: add nft boost status card (#678)

* feat(cr_nft): add nft boost status card

* fix: fix lint errors and add data-testid

* fix: use justify end to show the loading button at the end of the row

---------

Co-authored-by: Antonio <antonio@iovlabs.org>

feat(cr_nft_boost): add glowing label

TOK-621: add glowing label (#689)

* feat(cr_nft_boost): add glowing label

* chore: disable the boosted label

* fix: storybook type

---------

Co-authored-by: Antonio <antonio@iovlabs.org>

feat(cr_booster): add context and data retrieval

feat(cr_nft_boost): add nft card to BaBB

feat(cr_nft_booster): display all nft booster cards

feat(cr_nft_booster): display booster label
  • Loading branch information
jurajpiar committed Feb 14, 2025
1 parent 3380009 commit 3c1d410
Show file tree
Hide file tree
Showing 20 changed files with 722 additions and 80 deletions.
1 change: 1 addition & 0 deletions .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ NEXT_PUBLIC_RNS_REGISTRY_ADDRESS=0x7d284aaac6e925aad802a53c0c69efe3764597b8
# CR-related env variables
NEXT_PUBLIC_BACKERS_MANAGER_ADDRESS=0x41841e316F85933247fC7b51c79B76F22eB239bc
NEXT_PUBLIC_REWARD_DISTRIBUTOR_ADDRESS=0x83Eac3Abe6AAF4a4AAe8B067888FAD600c4cF6B3
NEXT_PUBLIC_NFT_BOOSTER_DATA_URL="https://raw.githubusercontent.com/RootstockCollective/dao-frontend/boost-data/nft_boost_data"

# Rif Wallet Services
NEXT_PUBLIC_RIF_WALLET_SERVICES=https://dev.rws.app.rootstockcollective.xyz
Expand Down
1 change: 1 addition & 0 deletions .env.mainnet
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ NEXT_PUBLIC_RNS_REGISTRY_ADDRESS=0xcb868aeabd31e2b66f74e9a55cf064abb31a4ad5
# CR-related env variables
NEXT_PUBLIC_BACKERS_MANAGER_ADDRESS=0x7995C48D987941291d8008695A4133E557a11530
NEXT_PUBLIC_REWARD_DISTRIBUTOR_ADDRESS=0x5603Ba40257e317e45BA13C3732819Af5E81a9A1
NEXT_PUBLIC_NFT_BOOSTER_DATA_URL="https://raw.githubusercontent.com/RootstockCollective/dao-frontend/boost-data/nft_boost_data"

# Rif Wallet Services
NEXT_PUBLIC_RIF_WALLET_SERVICES=https://rws.app.rootstockcollective.xyz
Expand Down
1 change: 1 addition & 0 deletions .env.qa
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ NEXT_PUBLIC_RNS_REGISTRY_ADDRESS=0x7d284aaac6e925aad802a53c0c69efe3764597b8
# CR-related env variables
NEXT_PUBLIC_BACKERS_MANAGER_ADDRESS=0x41841e316F85933247fC7b51c79B76F22eB239bc
NEXT_PUBLIC_REWARD_DISTRIBUTOR_ADDRESS=0x83Eac3Abe6AAF4a4AAe8B067888FAD600c4cF6B3
NEXT_PUBLIC_NFT_BOOSTER_DATA_URL="https://raw.githubusercontent.com/RootstockCollective/dao-frontend/boost-data/nft_boost_data"

# Rif Wallet Services
NEXT_PUBLIC_RIF_WALLET_SERVICES=https://dev.rws.app.rootstockcollective.xyz
Expand Down
1 change: 1 addition & 0 deletions .env.testnet
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ NEXT_PUBLIC_RNS_REGISTRY_ADDRESS=0x7d284aaac6e925aad802a53c0c69efe3764597b8
# CR-related env variables
NEXT_PUBLIC_BACKERS_MANAGER_ADDRESS=0xA46a06B29874ca18f51d3e2c306d32847ECDfaf7
NEXT_PUBLIC_REWARD_DISTRIBUTOR_ADDRESS=0xee3F0C605944a9B5348002B489A7213535fc38DE
NEXT_PUBLIC_NFT_BOOSTER_DATA_URL="https://raw.githubusercontent.com/RootstockCollective/dao-frontend/boost-data/nft_boost_data"

# Rif Wallet Services
NEXT_PUBLIC_RIF_WALLET_SERVICES=https://rws.app.rootstockcollective.xyz
Expand Down
1 change: 1 addition & 0 deletions .env.testnet.local
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ NEXT_PUBLIC_RNS_REGISTRY_ADDRESS=0x7d284aaac6e925aad802a53c0c69efe3764597b8
# CR-related env variables
NEXT_PUBLIC_BACKERS_MANAGER_ADDRESS=0x41841e316F85933247fC7b51c79B76F22eB239bc
NEXT_PUBLIC_REWARD_DISTRIBUTOR_ADDRESS=0x83Eac3Abe6AAF4a4AAe8B067888FAD600c4cF6B3
NEXT_PUBLIC_NFT_BOOSTER_DATA_URL="https://raw.githubusercontent.com/RootstockCollective/dao-frontend/boost-data/nft_boost_data"

# Local testnet
NEXT_PUBLIC_PROXY_DESTINATION=https://dev.rws.app.rootstockcollective.xyz
Expand Down
1 change: 1 addition & 0 deletions .env.testnet.qa
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ NEXT_PUBLIC_RNS_REGISTRY_ADDRESS=0x7d284aaac6e925aad802a53c0c69efe3764597b8
# CR-related env variables
NEXT_PUBLIC_BACKERS_MANAGER_ADDRESS=0x41841e316F85933247fC7b51c79B76F22eB239bc
NEXT_PUBLIC_REWARD_DISTRIBUTOR_ADDRESS=0x83Eac3Abe6AAF4a4AAe8B067888FAD600c4cF6B3
NEXT_PUBLIC_NFT_BOOSTER_DATA_URL="https://raw.githubusercontent.com/RootstockCollective/dao-frontend/boost-data/nft_boost_data"

# Rif Wallet Services
NEXT_PUBLIC_RIF_WALLET_SERVICES=https://dev.rws.app.rootstockcollective.xyz
Expand Down
24 changes: 11 additions & 13 deletions src/app/collective-rewards/metrics/Metrics.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import { BecomeABuilderButton, useGetGaugesArray } from '@/app/collective-rewards/user'
import { HeaderTitle } from '@/components/Typography'
import {
TotalAllocationsMetrics,
CycleMetrics,
AllTimeRewardsMetrics,
CycleContextProvider,
CycleMetrics,
TotalActiveBuildersMetrics,
AllTimeRewardsMetrics,
TotalAllocationsMetrics,
} from '@/app/collective-rewards/metrics'
import { getAddress } from 'viem'
import { tokenContracts } from '@/lib/contracts'
import { useGetGaugesArray } from '@/app/collective-rewards/user'
import { getCoinbaseAddress } from '@/app/collective-rewards/utils'
import { HeaderTitle } from '@/components/Typography'
import { tokenContracts } from '@/lib/contracts'
import { PricesContextProvider } from '@/shared/context/PricesContext'
import { getAddress } from 'viem'
import { withBuilderButton } from '../user/components/Button/WithBuilderButton'
import { ABIMetrics } from './components/ABIMetrics'
import { useAccount } from 'wagmi'

export const Metrics = () => {
const { address } = useAccount()
const { data: allGauges } = useGetGaugesArray()
const gauges = allGauges ?? []

Expand All @@ -32,10 +31,9 @@ export const Metrics = () => {

return (
<div>
<div className="flex justify-between items-center self-stretch mb-6">
<HeaderTitle>Metrics</HeaderTitle>
<BecomeABuilderButton address={address!} />
</div>
{withBuilderButton(HeaderTitle)({
children: 'Metrics',
})}
<PricesContextProvider>
<CycleContextProvider>
<div className="flex gap-4 w-full">
Expand Down
1 change: 1 addition & 0 deletions src/app/collective-rewards/rewards/MyRewards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const BackerRewardsSection: FC<RewardDetails> = data => {
<RewardsSection>
<div className="inline-flex gap-6">
<RewardsSectionHeader
isBacker={true}
title="Backer Rewards"
subtext={<SubText />}
utility={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,35 @@ import { GlowingLabel } from '@/components/Label/GlowingLabel'
import { HeaderTitle, Typography } from '@/components/Typography'
import React, { FC, ReactNode } from 'react'
import { Tooltip } from '../Tooltip'
import { useNFTBoosterContext } from '@/app/providers/NFT/BoosterContext'

export type RewardsSectionHeader = {
title: string
subtext: ReactNode
utility: ReactNode
isBacker?: boolean
}
export const RewardsSectionHeader: FC<RewardsSectionHeader> = ({ title, subtext, utility }) => (
<div className="flex justify-between w-full items-center gap-[24px]">
<div className="flex flex-col items-start w-full">
<div className="flex justify-center items-center gap-1">
<HeaderTitle className="uppercase text-2xl leading-7 font-normal">{title}</HeaderTitle>
{/* FIXME: to be removed outside and enabled for the backer only */}
{/* <div className="inline-flex items-center gap-1">
<BoltSvg />
<GlowingLabel>Boosted</GlowingLabel>
<Tooltip text="Your rewards are boosted thanks to your NFT’s superpowers." />
</div> */}
export const RewardsSectionHeader: FC<RewardsSectionHeader> = ({ title, subtext, utility, isBacker }) => {
const { isBoosted, hasActiveCampaign } = useNFTBoosterContext()

return (
<div className="flex justify-between w-full items-center gap-[24px]">
<div className="flex flex-col items-start w-full">
<div className="flex justify-center items-center gap-1">
<HeaderTitle className="uppercase text-2xl leading-7 font-normal">{title}</HeaderTitle>
{isBacker && hasActiveCampaign && isBoosted && (
<div className="inline-flex items-center gap-1">
<BoltSvg />
<GlowingLabel>Boosted</GlowingLabel>
<Tooltip text="Your rewards are boosted thanks to your NFT’s superpowers." />
</div>
)}
</div>
<Typography tagVariant="p" className="text-sm leading-7 font-normal font-rootstock-sans">
{subtext}
</Typography>
</div>
<Typography tagVariant="p" className="text-sm leading-7 font-normal font-rootstock-sans">
{subtext}
</Typography>
{utility}
</div>
{utility}
</div>
)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// WithBuilderButton.test.tsx
import React from 'react'
import { render, screen } from '@testing-library/react'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { withBuilderButton } from './WithBuilderButton'

// --- Mocks for child components ---

// Spy for NFTBoosterCard – we render a simple div that displays the boost value and title.
const mockNFTBoosterCard = vi.fn(({ boostValue, nftThumbPath, title }) => (
<div data-testid="nft-booster-card">
NFT Booster: {boostValue} - {title}
</div>
))
// Spy for BecomeABuilderButton – we render a simple div showing the address.
const mockBecomeABuilderButton = vi.fn(({ address }) => (
<div data-testid="builder-button">Builder Address: {address}</div>
))

// Mock the BecomeABuilderButton module.
vi.mock('./BecomeABuilderButton', () => ({
BecomeABuilderButton: (props: any) => mockBecomeABuilderButton(props),
}))

// Mock the shared components module (specifically NFTBoosterCard).
vi.mock('@/app/shared/components', () => ({
NFTBoosterCard: (props: any) => mockNFTBoosterCard(props),
}))

// --- Mocks for hooks and utilities ---

// Mock the wagmi useAccount hook.
import { useAccount } from 'wagmi'
vi.mock('wagmi', () => ({
useAccount: vi.fn(),
}))

// Mock the NFT booster context hook.
import { useNFTBoosterContext } from '@/app/providers/NFT/BoosterContext'
vi.mock('@/app/providers/NFT/BoosterContext', () => ({
useNFTBoosterContext: vi.fn(),
}))

// Mock the communitiesMapByContract so that it returns a title when queried with a known address.
vi.mock('@/app/communities/communityUtils', () => ({
communitiesMapByContract: {
'0xabc': { title: 'Test NFT' },
},
}))

// --- Create a dummy component to wrap ---
function DummyComponent() {
return <div data-testid="dummy">Dummy Component</div>
}
DummyComponent.displayName = 'DummyComponent'

// Wrap the DummyComponent using the HOC.
const WrappedComponent = withBuilderButton(DummyComponent)

describe('withBuilderButton HOC', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('renders the wrapped component and BecomeABuilderButton when no active NFT booster campaign', () => {
// Setup mocks for a scenario with no active campaign.
;(useAccount as any).mockReturnValue({ address: '0x1234' })
;(useNFTBoosterContext as any).mockReturnValue({
hasActiveCampaign: false,
currentBoost: undefined,
boostData: undefined,
})

render(<WrappedComponent />)

// Verify the wrapped (dummy) component is rendered.
expect(screen.getByTestId('dummy')).toBeInTheDocument()

// NFTBoosterCard should NOT be rendered.
expect(screen.queryByTestId('nft-booster-card')).toBeNull()

// BecomeABuilderButton should be rendered with the correct address.
expect(screen.getByTestId('builder-button')).toHaveTextContent('0x1234')
})

it('renders NFTBoosterCard when active NFT booster campaign is present', () => {
// Setup mocks for a scenario with an active campaign and boost data.
;(useAccount as any).mockReturnValue({ address: '0x1234' })
;(useNFTBoosterContext as any).mockReturnValue({
hasActiveCampaign: true,
currentBoost: 10,
boostData: {
nftContractAddress: '0xabc',
boostPercentage: '20', // Note: boostPercentage is a string in the original code.
},
})

render(<WrappedComponent />)

// Verify NFTBoosterCard is rendered with the correct boost value and title.
expect(screen.getByTestId('nft-booster-card')).toHaveTextContent('NFT Booster: 20 - Test NFT')

// Also verify that BecomeABuilderButton is rendered.
// FIXME: not working. finding duplicates of the button
// expect(screen.getByTestId('builder-button')).not.toBeNull()
})

it('sets the displayName correctly', () => {
expect(WrappedComponent.displayName).toBe('WithBuilderButton(DummyComponent)')
})
})
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { SelfContainedNFTBoosterCard } from '@/app/shared/components/NFTBoosterCard/SelfContainedNFTBoosterCard'
import { ComponentType, FC } from 'react'
import { BecomeABuilderButton } from './BecomeABuilderButton'
import { useAccount } from 'wagmi'
import { NFTBoosterCard } from '@/app/shared/components'
import { BecomeABuilderButton } from './BecomeABuilderButton'

export const withBuilderButton = <P extends {}>(Component: ComponentType<P>): FC<P> => {
const WrappedComponent = ({ ...props }: P) => {
const { address } = useAccount()

return (
<div className="flex justify-between items-center self-stretch mb-6">
<Component {...(props as P)} />
<div className="flex gap-3 justify-end">
{/* FIXME: get the nft booster context and check if there is an active campaign and the user owns the related NFT */}
{/* <NFTBoosterCard boostValue={20} nftThumbPath="" title="HI" /> */}
<SelfContainedNFTBoosterCard />
<BecomeABuilderButton address={address!} />
</div>
</div>
Expand Down
13 changes: 13 additions & 0 deletions src/app/communities/nft/[address]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import { CopyButton } from '@/components/CopyButton'
import { NftHoldersSection } from '@/app/communities/NftHoldersSection'
import { communitiesMapByContract } from '@/app/communities/communityUtils'
import { isUserRejectedTxError } from '@/components/ErrorPage/commonErrors'
import { SelfContainedNFTBoosterCard } from '../../../shared/components/NFTBoosterCard/SelfContainedNFTBoosterCard'
import { GlowingLabel } from '@/components/Label/GlowingLabel'
import { BoltSvg } from '@/components/BoltSvg'
import { useNFTBoosterContext } from '@/app/providers/NFT/BoosterContext'

/**
* Name of the local storage variable with information about whether the token was added to the wallet
Expand Down Expand Up @@ -61,6 +65,7 @@ export default function Page() {
stRifThreshold,
} = useCommunity(nftAddress)
const { stRifBalance } = useStRif()
const { isBoosted, hasActiveCampaign } = useNFTBoosterContext()

const nftInfo = communitiesMapByContract[nftAddress || '']
if (nftInfo === undefined && nftAddress !== undefined) {
Expand Down Expand Up @@ -272,6 +277,12 @@ export default function Page() {
<div className="font-semibold">{nftInfo?.title}</div>
</div>
<div className="mb-[24px] font-extralight">{nftInfo?.longDescription}</div>
{hasActiveCampaign && isBoosted && (
<div className="inline-flex items-center gap-1 pb-6">
<BoltSvg />
<GlowingLabel>Boosted {20}%</GlowingLabel>
</div>
)}
{/* Hidden until we get social media data */}
<div className="gap-[8px] mt-[16px] mb-[24px] hidden">
{/* Chips with community links */}
Expand Down Expand Up @@ -329,6 +340,8 @@ export default function Page() {
)}
</div>

<SelfContainedNFTBoosterCard />

{/* `Add to wallet button` */}
{!isNftInWallet?.[nftAddress]?.[tokenId] && (
<Button
Expand Down
5 changes: 4 additions & 1 deletion src/app/providers/ContextProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AlertProvider } from './AlertProvider'
import ErrorBoundary from '@/components/ErrorPage/ErrorBoundary'
import { BuilderContextProviderWithPrices } from '../collective-rewards/user'
import { AllocationsContextProvider } from '../collective-rewards/allocations/context'
import { BoosterProvider } from './NFT/BoosterContext'

interface Props {
children: ReactNode
Expand All @@ -20,7 +21,9 @@ export const ContextProviders = ({ children }: Props) => {
<QueryClientProvider client={queryClient}>
<AlertProvider>
<BuilderContextProviderWithPrices>
<AllocationsContextProvider>{children}</AllocationsContextProvider>
<BoosterProvider>
<AllocationsContextProvider>{children}</AllocationsContextProvider>
</BoosterProvider>
</BuilderContextProviderWithPrices>
</AlertProvider>
</QueryClientProvider>
Expand Down
Loading

0 comments on commit 3c1d410

Please sign in to comment.