Skip to content

Commit

Permalink
💯 UI/UX CRT improvement (#6308)
Browse files Browse the repository at this point in the history
* Add number of items to tabs of channel view

* Extra info for market when revenue share is active

* Restrict revenue share duration to 2 weeks

* Add QS pagination for crt market

* Better signalize revenue share money block on channel view
  • Loading branch information
ikprk authored May 20, 2024
1 parent e663bb9 commit 4797ef0
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@ import { BuyMarketTokenModal } from '@/components/_crt/BuyMarketTokenModal'

type BuyFromMarketButtonProps = {
tokenId: string
hasActiveRevenueShare?: boolean
}

export const BuyFromMarketButton = ({ tokenId }: BuyFromMarketButtonProps) => {
export const BuyFromMarketButton = ({ tokenId, hasActiveRevenueShare }: BuyFromMarketButtonProps) => {
const [showModal, setShowModal] = useState(false)
return (
<>
<BuyMarketTokenModal tokenId={tokenId} show={showModal} onClose={() => setShowModal(false)} />
<ProtectedActionWrapper title="You want to buy tokens?" description="Sign in to buy">
<Button size="large" fullWidth onClick={() => setShowModal(true)}>
<Button
variant={hasActiveRevenueShare ? 'warning' : 'primary'}
size="large"
fullWidth
onClick={() => setShowModal(true)}
>
Buy
</Button>
</ProtectedActionWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
useGetCreatorTokenHoldersQuery,
useGetFullCreatorTokenQuery,
} from '@/api/queries/__generated__/creatorTokens.generated'
import { SvgAlertsWarning24 } from '@/assets/icons'
import { NumberFormat } from '@/components/NumberFormat'
import { Text } from '@/components/Text'
import { Tooltip } from '@/components/Tooltip'
import { AmmModalFormTemplate } from '@/components/_crt/AmmModalTemplates'
import { AmmModalSummaryTemplate } from '@/components/_crt/AmmModalTemplates/AmmModalSummaryTemplate'
import { BuyMarketTokenSuccess } from '@/components/_crt/BuyMarketTokenModal/steps/BuyMarketTokenSuccess'
Expand Down Expand Up @@ -353,6 +355,8 @@ export const BuyMarketTokenModal = ({ tokenId, onClose: _onClose, show }: BuySal
if (activeStep === BUY_MARKET_TOKEN_STEPS.form) {
setPrimaryButtonProps({
text: 'Continue',
disabled: hasActiveRevenueShare,
variant: hasActiveRevenueShare ? 'warning' : undefined,
onClick: () => {
if (hasActiveRevenueShare) {
displaySnackbar({
Expand Down Expand Up @@ -405,6 +409,13 @@ export const BuyMarketTokenModal = ({ tokenId, onClose: _onClose, show }: BuySal
secondaryButton={secondaryButton}
confetti={activeStep === BUY_MARKET_TOKEN_STEPS.success && smMatch}
noContentPadding={activeStep === BUY_MARKET_TOKEN_STEPS.conditions}
additionalActionsNode={
hasActiveRevenueShare ? (
<Tooltip text="During revenue share you are unable to trade on market. Please wait until it ends to make new transactions.">
<SvgAlertsWarning24 />
</Tooltip>
) : null
}
>
{activeStep === BUY_MARKET_TOKEN_STEPS.form && (
<AmmModalFormTemplate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { SellOnMarketButton } from '@/components/_crt/SellOnMarketButton/SellOnM
import { DetailsContent } from '@/components/_nft/NftTile'
import { useMediaMatch } from '@/hooks/useMediaMatch'
import { hapiBnToTokenNumber } from '@/joystream-lib/utils'
import { useJoystreamStore } from '@/providers/joystream/joystream.store'
import { calcBuyMarketPricePerToken } from '@/utils/crts'
import { SentryLogger } from '@/utils/logs'
import { formatDate } from '@/utils/time'
Expand Down Expand Up @@ -109,6 +110,9 @@ const InactiveDetails = () => {
}

const MarketDetails = ({ token }: { token: FullCreatorTokenFragment }) => {
const currentBlockRef = useRef(useJoystreamStore((store) => store.currentBlock))
const activeRevenueShare = token.revenueShares.find((rS) => !rS.finalized)
const hasActiveRevenueShare = (activeRevenueShare?.endsAt ?? 0) > currentBlockRef.current
const calculateSlippageAmount = useCallback(
(amount: number) => {
const currentAmm = token?.ammCurves.find((amm) => !amm.finalized)
Expand All @@ -132,8 +136,8 @@ const MarketDetails = ({ token }: { token: FullCreatorTokenFragment }) => {
tooltipText="Price of each incremental unit purchased or sold depends on overall quantity of tokens transacted, the actual average price per unit for the entire purchase or sale will differ from the price displayed for the first unit transacted."
/>
<FlexBox equalChildren width="100%" gap={2}>
<SellOnMarketButton tokenId={token.id} />
<BuyFromMarketButton tokenId={token.id} />
<SellOnMarketButton hasActiveRevenueShare={hasActiveRevenueShare} tokenId={token.id} />
<BuyFromMarketButton hasActiveRevenueShare={hasActiveRevenueShare} tokenId={token.id} />
</FlexBox>

<SupplyLine>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@ import { SellTokenModal } from '@/components/_crt/SellTokenModal'

type SellOnMarketButtonProps = {
tokenId: string
hasActiveRevenueShare?: boolean
}

export const SellOnMarketButton = ({ tokenId }: SellOnMarketButtonProps) => {
export const SellOnMarketButton = ({ tokenId, hasActiveRevenueShare }: SellOnMarketButtonProps) => {
const [showModal, setShowModal] = useState(false)
return (
<>
<SellTokenModal tokenId={tokenId} show={showModal} onClose={() => setShowModal(false)} />
<ProtectedActionWrapper title="You want to sell tokens?" description="Sign in to sell">
<Button variant="secondary" fullWidth size="large" onClick={() => setShowModal(true)}>
<Button
variant={hasActiveRevenueShare ? 'warning-secondary' : 'secondary'}
fullWidth
size="large"
onClick={() => setShowModal(true)}
>
Sell
</Button>
</ProtectedActionWrapper>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import BN from 'bn.js'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import { useForm } from 'react-hook-form'

import { useGetFullCreatorTokenQuery } from '@/api/queries/__generated__/creatorTokens.generated'
import { SvgAlertsWarning24 } from '@/assets/icons'
import { NumberFormat, formatNumberShort } from '@/components/NumberFormat'
import { Text } from '@/components/Text'
import { Tooltip } from '@/components/Tooltip'
import { AmmModalFormTemplate } from '@/components/_crt/AmmModalTemplates'
import { AmmModalSummaryTemplate } from '@/components/_crt/AmmModalTemplates/AmmModalSummaryTemplate'
import { DialogModal } from '@/components/_overlays/DialogModal'
Expand All @@ -13,6 +15,7 @@ import { useGetTokenBalance } from '@/hooks/useGetTokenBalance'
import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
import { hapiBnToTokenNumber, tokenNumberToHapiBn } from '@/joystream-lib/utils'
import { useFee, useJoystream } from '@/providers/joystream'
import { useJoystreamStore } from '@/providers/joystream/joystream.store'
import { useNetworkUtils } from '@/providers/networkUtils/networkUtils.hooks'
import { useSnackbar } from '@/providers/snackbars'
import { useTransaction } from '@/providers/transactions/transactions.hooks'
Expand All @@ -38,6 +41,7 @@ export const SellTokenModal = ({ tokenId, onClose: _onClose, show }: SellTokenMo
const { trackAMMTokensSold } = useSegmentAnalytics()
const { displaySnackbar } = useSnackbar()
const { fullFee } = useFee('sellTokenOnMarketTx', ['1', '1', '2', '10000000'])
const currentBlockRef = useRef(useJoystreamStore((store) => store.currentBlock))
const { data, loading } = useGetFullCreatorTokenQuery({
variables: {
id: tokenId,
Expand All @@ -46,7 +50,8 @@ export const SellTokenModal = ({ tokenId, onClose: _onClose, show }: SellTokenMo
SentryLogger.error('Failed to fetch token data', 'SellTokenModal', { error })
},
})
const hasActiveRevenueShare = data?.creatorTokenById?.revenueShares.some((rS) => !rS.finalized)
const activeRevenueShare = data?.creatorTokenById?.revenueShares.find((rS) => !rS.finalized)
const hasActiveRevenueShare = (activeRevenueShare?.endsAt ?? 0) > currentBlockRef.current

const currentAmm = data?.creatorTokenById?.currentAmmSale
const ammBalance = currentAmm ? +currentAmm.mintedByAmm - +currentAmm.burnedByAmm : 0
Expand Down Expand Up @@ -263,13 +268,22 @@ export const SellTokenModal = ({ tokenId, onClose: _onClose, show }: SellTokenMo
title={`Sell $${title}`}
show={show}
onExitClick={onClose}
additionalActionsNode={
hasActiveRevenueShare ? (
<Tooltip text="During revenue share you are unable to trade on market. Please wait until it ends to make new transactions.">
<SvgAlertsWarning24 />
</Tooltip>
) : null
}
secondaryButton={{
text: isFormStep ? 'Cancel' : 'Back',
onClick: isFormStep ? onClose : () => setStep('form'),
}}
primaryButton={{
text: isFormStep ? 'Continue' : `Sell $${title}`,
onClick: isFormStep ? onFormSubmit : onTransactionSubmit,
disabled: hasActiveRevenueShare,
variant: hasActiveRevenueShare ? 'warning' : undefined,
}}
>
{step === 'form' ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,12 @@ export const StartRevenueShare = ({ token, onClose, show }: StartRevenueSharePro
)}`
}

const twoWeeksDate = addDaysToDate(14, new Date())

if (valueTimestamp.getTime() > twoWeeksDate.getTime()) {
return 'Maximal revenue share duration is 2 weeks.'
}

return true
},
}}
Expand Down
27 changes: 27 additions & 0 deletions packages/atlas/src/hooks/usePagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useCallback, useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'

export type UsePaginationOptions = {
initialPerPage?: number
initialCurrentPage?: number
currentTab?: number
}

export const useQueryPagination = (opts?: UsePaginationOptions) => {
const [getSearchParams, setSearchParams] = useSearchParams()
const _perPage = +(getSearchParams.get('perPage') ?? opts?.initialPerPage ?? 10)
const _currentPage = +(getSearchParams.get('currentPage') ?? opts?.initialCurrentPage ?? 0)
const [perPage, setPerPage] = useState(_perPage)
const [currentPage, setCurrentPage] = useState(_currentPage)

useEffect(() => {
setSearchParams({ currentPage: String(currentPage), perPage: String(perPage) }, { replace: true })
}, [currentPage, perPage, setSearchParams])

const resetPagination = useCallback(() => {
setCurrentPage(opts?.initialCurrentPage ?? 0)
setPerPage(opts?.initialCurrentPage ?? 10)
}, [opts?.initialCurrentPage, setCurrentPage, setPerPage])

return { currentPage, setCurrentPage, perPage, setPerPage, resetPagination }
}
14 changes: 5 additions & 9 deletions packages/atlas/src/hooks/useTokensPagniation.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { useState } from 'react'

import { CreatorTokenOrderByInput, CreatorTokenWhereInput } from '@/api/queries/__generated__/baseTypes.generated'
import {
useGetBasicCreatorTokensQuery,
useGetCreatorTokensCountQuery,
} from '@/api/queries/__generated__/creatorTokens.generated'
import { SentryLogger } from '@/utils/logs'
import { usePagination } from '@/views/viewer/ChannelView/ChannelView.hooks'

import { useQueryPagination } from './usePagination'

export const useTokensPagination = ({
where,
Expand All @@ -17,16 +16,15 @@ export const useTokensPagination = ({
orderBy?: CreatorTokenOrderByInput
initialPageSize?: number
}) => {
const pagination = usePagination(0)
const [perPage, setPerPage] = useState(initialPageSize)
const pagination = useQueryPagination({ initialPerPage: initialPageSize })

const { data, loading } = useGetBasicCreatorTokensQuery({
notifyOnNetworkStatusChange: true,
variables: {
where,
orderBy,
offset: pagination.currentPage * perPage,
limit: perPage,
offset: pagination.currentPage * pagination.perPage,
limit: pagination.perPage,
},
onError: (error) => {
SentryLogger.error('Failed to fetch tokens query', 'useTokensPagination', error)
Expand All @@ -43,7 +41,5 @@ export const useTokensPagination = ({
tokens: data?.creatorTokens,
totalCount: countData?.creatorTokensConnection.totalCount ?? 0,
isLoading: loading || loadingCount,
setPerPage,
perPage,
}
}
37 changes: 36 additions & 1 deletion packages/atlas/src/views/viewer/ChannelView/ChannelView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { useNavigate } from 'react-router'
import { useParams, useSearchParams } from 'react-router-dom'

import { useChannelNftCollectors, useFullChannel } from '@/api/hooks/channel'
import { useVideoCount } from '@/api/hooks/video'
import { OwnedNftOrderByInput, VideoOrderByInput } from '@/api/queries/__generated__/baseTypes.generated'
import { useGetNftsCountQuery } from '@/api/queries/__generated__/nfts.generated'
import { SvgActionCheck, SvgActionFilters, SvgActionFlag, SvgActionMore, SvgActionPlus } from '@/assets/icons'
import { ChannelTitle } from '@/components/ChannelTitle'
import { EmptyFallback } from '@/components/EmptyFallback'
Expand Down Expand Up @@ -62,6 +64,7 @@ import { ChannelAbout, ChannelNfts, ChannelVideos } from './ChannelViewTabs'
import { TABS } from './utils'

export const INITIAL_TILES_PER_ROW = 4
const USER_TIMESTAMP = new Date()

export const ChannelView: FC = () => {
const [searchParams, setSearchParams] = useSearchParams()
Expand All @@ -88,6 +91,35 @@ export const ChannelView: FC = () => {
const filteredTabs = TABS.filter((tab) =>
tab === 'Token' ? !!tab && (isChannelOwner || !!channel?.creatorToken?.token.id) : !!tab
)
const { videoCount } = useVideoCount({
where: {
channel: {
id_eq: id,
},
isPublic_eq: true,
createdAt_lt: USER_TIMESTAMP,
isCensored_eq: false,
thumbnailPhoto: {
isAccepted_eq: true,
},
media: {
isAccepted_eq: true,
},
},
})
const { data: nftCountData } = useGetNftsCountQuery({
variables: {
where: {
createdAt_lte: USER_TIMESTAMP,
video: {
channel: {
id_eq: id,
},
isPublic_eq: !isChannelOwner || undefined,
},
},
},
})

// At mount set the tab from the search params
// This hook has to come before useRedirectMigratedContent so it doesn't messes it's navigate call
Expand Down Expand Up @@ -186,7 +218,10 @@ export const ChannelView: FC = () => {

const handleOnResizeGrid = (sizes: number[]) => setTilesPerRow(sizes.length)

const mappedTabs = filteredTabs.map((tab) => ({ name: tab, badgeNumber: 0 }))
const mappedTabs = filteredTabs.map((tab) => ({
name: tab,
pillText: tab === 'Videos' ? videoCount : tab === 'NFTs' ? nftCountData?.ownedNftsConnection.totalCount : undefined,
}))

const getChannelContent = (tab: (typeof TABS)[number]) => {
switch (tab) {
Expand Down

0 comments on commit 4797ef0

Please sign in to comment.