From a5a8035ebce2fcade2a9bd70f75ac94f3ad7169c Mon Sep 17 00:00:00 2001 From: ikprk <168457495+ikprk@users.noreply.github.com> Date: Mon, 20 May 2024 21:43:20 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=AF=20UI/UX=20CRT=20improvement=20(#63?= =?UTF-8?q?08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../BuyFromMarketButton.tsx | 10 ++++- .../BuyMarketTokenModal.tsx | 11 ++++++ .../_crt/CrtStatusWidget/CrtStatusWidget.tsx | 8 +++- .../SellOnMarketButton/SellOnMarketButton.tsx | 10 ++++- .../_crt/SellTokenModal/SellTokenModal.tsx | 18 ++++++++- .../StartRevenueShareModal.tsx | 6 +++ packages/atlas/src/hooks/usePagination.ts | 27 ++++++++++++++ .../atlas/src/hooks/useTokensPagniation.ts | 14 +++---- .../views/viewer/ChannelView/ChannelView.tsx | 37 ++++++++++++++++++- 9 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 packages/atlas/src/hooks/usePagination.ts diff --git a/packages/atlas/src/components/_crt/BuyFromMarketButton/BuyFromMarketButton.tsx b/packages/atlas/src/components/_crt/BuyFromMarketButton/BuyFromMarketButton.tsx index e0d56c6a94..61587e88bc 100644 --- a/packages/atlas/src/components/_crt/BuyFromMarketButton/BuyFromMarketButton.tsx +++ b/packages/atlas/src/components/_crt/BuyFromMarketButton/BuyFromMarketButton.tsx @@ -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 ( <> setShowModal(false)} /> - diff --git a/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx b/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx index c8daaf9e54..bfafcad780 100644 --- a/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx +++ b/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx @@ -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' @@ -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({ @@ -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 ? ( + + + + ) : null + } > {activeStep === BUY_MARKET_TOKEN_STEPS.form && ( { } 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) @@ -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." /> - - + + diff --git a/packages/atlas/src/components/_crt/SellOnMarketButton/SellOnMarketButton.tsx b/packages/atlas/src/components/_crt/SellOnMarketButton/SellOnMarketButton.tsx index d4444c67cf..3e9ee174d6 100644 --- a/packages/atlas/src/components/_crt/SellOnMarketButton/SellOnMarketButton.tsx +++ b/packages/atlas/src/components/_crt/SellOnMarketButton/SellOnMarketButton.tsx @@ -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 ( <> setShowModal(false)} /> - diff --git a/packages/atlas/src/components/_crt/SellTokenModal/SellTokenModal.tsx b/packages/atlas/src/components/_crt/SellTokenModal/SellTokenModal.tsx index 2327e4cde6..2a2c4e850e 100644 --- a/packages/atlas/src/components/_crt/SellTokenModal/SellTokenModal.tsx +++ b/packages/atlas/src/components/_crt/SellTokenModal/SellTokenModal.tsx @@ -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' @@ -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' @@ -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, @@ -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 @@ -263,6 +268,13 @@ export const SellTokenModal = ({ tokenId, onClose: _onClose, show }: SellTokenMo title={`Sell $${title}`} show={show} onExitClick={onClose} + additionalActionsNode={ + hasActiveRevenueShare ? ( + + + + ) : null + } secondaryButton={{ text: isFormStep ? 'Cancel' : 'Back', onClick: isFormStep ? onClose : () => setStep('form'), @@ -270,6 +282,8 @@ export const SellTokenModal = ({ tokenId, onClose: _onClose, show }: SellTokenMo primaryButton={{ text: isFormStep ? 'Continue' : `Sell $${title}`, onClick: isFormStep ? onFormSubmit : onTransactionSubmit, + disabled: hasActiveRevenueShare, + variant: hasActiveRevenueShare ? 'warning' : undefined, }} > {step === 'form' ? ( diff --git a/packages/atlas/src/components/_crt/StartRevenueShareModal/StartRevenueShareModal.tsx b/packages/atlas/src/components/_crt/StartRevenueShareModal/StartRevenueShareModal.tsx index 3c84e94929..ec4d41ebf2 100644 --- a/packages/atlas/src/components/_crt/StartRevenueShareModal/StartRevenueShareModal.tsx +++ b/packages/atlas/src/components/_crt/StartRevenueShareModal/StartRevenueShareModal.tsx @@ -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 }, }} diff --git a/packages/atlas/src/hooks/usePagination.ts b/packages/atlas/src/hooks/usePagination.ts new file mode 100644 index 0000000000..c86c985a74 --- /dev/null +++ b/packages/atlas/src/hooks/usePagination.ts @@ -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 } +} diff --git a/packages/atlas/src/hooks/useTokensPagniation.ts b/packages/atlas/src/hooks/useTokensPagniation.ts index 8f03a7d88a..ef72a9a6ec 100644 --- a/packages/atlas/src/hooks/useTokensPagniation.ts +++ b/packages/atlas/src/hooks/useTokensPagniation.ts @@ -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, @@ -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) @@ -43,7 +41,5 @@ export const useTokensPagination = ({ tokens: data?.creatorTokens, totalCount: countData?.creatorTokensConnection.totalCount ?? 0, isLoading: loading || loadingCount, - setPerPage, - perPage, } } diff --git a/packages/atlas/src/views/viewer/ChannelView/ChannelView.tsx b/packages/atlas/src/views/viewer/ChannelView/ChannelView.tsx index 655f2a783a..73b10e6035 100644 --- a/packages/atlas/src/views/viewer/ChannelView/ChannelView.tsx +++ b/packages/atlas/src/views/viewer/ChannelView/ChannelView.tsx @@ -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' @@ -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() @@ -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 @@ -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) {