Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💯 UI/UX CRT improvement #6308

Merged
merged 5 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading