diff --git a/src/components/Common/FullWidthButton.css b/src/components/Common/FullWidthButton.css index 7b8549993..230f08ed1 100644 --- a/src/components/Common/FullWidthButton.css +++ b/src/components/Common/FullWidthButton.css @@ -12,3 +12,7 @@ a.ui.fluid.primary.button.FullWidthButton { align-items: center; justify-content: center; } + +.FullWidthButton__Loader { + position: relative; +} diff --git a/src/components/Common/FullWidthButton.tsx b/src/components/Common/FullWidthButton.tsx index 0fe891f11..c5acb463a 100644 --- a/src/components/Common/FullWidthButton.tsx +++ b/src/components/Common/FullWidthButton.tsx @@ -1,28 +1,38 @@ import classNames from 'classnames' import { Button } from 'decentraland-ui/dist/components/Button/Button' +import { Loader } from 'decentraland-ui/dist/components/Loader/Loader' import './FullWidthButton.css' interface Props { - onClick?: (param?: unknown) => void + onClick?: () => void children: React.ReactNode className?: string - link?: string + href?: string newWindow?: boolean + loading?: boolean } -const FullWidthButton = ({ onClick, children, className, link, newWindow = false }: Props) => { +// TODO: FullWidthButton should render Link when href is provided +const FullWidthButton = ({ onClick, children, className, href, newWindow = false, loading }: Props) => { return ( ) } diff --git a/src/components/Home/ActiveCommunityGrants.tsx b/src/components/Home/ActiveCommunityGrants.tsx index 173dd0586..295e06b4a 100644 --- a/src/components/Home/ActiveCommunityGrants.tsx +++ b/src/components/Home/ActiveCommunityGrants.tsx @@ -44,7 +44,7 @@ const ActiveCommunityGrants = () => { ))} )} - + {t('page.home.active_community_grants.view_all_grants')} diff --git a/src/components/Home/OpenProposals.tsx b/src/components/Home/OpenProposals.tsx index 026e7cb50..cbae56678 100644 --- a/src/components/Home/OpenProposals.tsx +++ b/src/components/Home/OpenProposals.tsx @@ -93,7 +93,7 @@ const OpenProposals = ({ endingSoonProposals, isLoadingProposals }: Props) => { )} - + {t('page.home.open_proposals.view_all_proposals')} diff --git a/src/components/Profile/CoAuthoringTab.tsx b/src/components/Profile/CoAuthoringTab.tsx index ce966c598..d7110a053 100644 --- a/src/components/Profile/CoAuthoringTab.tsx +++ b/src/components/Profile/CoAuthoringTab.tsx @@ -3,13 +3,8 @@ import useAuthContext from 'decentraland-gatsby/dist/context/Auth/useAuthContext import { CoauthorAttributes } from '../../entities/Coauthor/types' import { isSameAddress } from '../../entities/Snapshot/utils' import useFormatMessage from '../../hooks/useFormatMessage' -import usePaginatedProposals from '../../hooks/usePaginatedProposals' -import Empty from '../Common/Empty' -import FullWidthButton from '../Common/FullWidthButton' -import SkeletonBars from '../Common/SkeletonBars' -import Watermelon from '../Icon/Watermelon' -import ProposalCreatedItem from './ProposalCreatedItem' +import { ProposalCreatedList } from './ProposalCreatedList' interface Props { address?: string @@ -23,43 +18,21 @@ const CoAuthoringTab = ({ address, pendingCoauthorRequests }: Props) => { const isLoggedUserProfile = isSameAddress(account, address || '') const user = isLoggedUserProfile ? account : address - const { proposals, hasMoreProposals, loadMore, isLoadingProposals } = usePaginatedProposals({ - load: !!user, - ...(!!user && { user: user?.toLowerCase() }), - coauthor: true, - }) - return ( - <> - {isLoadingProposals && } - {!isLoadingProposals && ( - <> - {proposals.length > 0 ? ( - proposals.map((proposal) => ( - req.proposal_id === proposal.id)} - /> - )) - ) : ( - } - description={ - isLoggedUserProfile - ? t('page.profile.activity.coauthoring.empty_logged_user') - : t('page.profile.activity.coauthoring.empty') - } - /> - )} - - )} - {!isLoadingProposals && hasMoreProposals && ( - {t('page.profile.activity.button')} - )} - + ) } diff --git a/src/components/Profile/ProposalCreatedList.tsx b/src/components/Profile/ProposalCreatedList.tsx new file mode 100644 index 000000000..4fadbcba2 --- /dev/null +++ b/src/components/Profile/ProposalCreatedList.tsx @@ -0,0 +1,58 @@ +import { CoauthorAttributes } from '../../entities/Coauthor/types' +import useFormatMessage from '../../hooks/useFormatMessage' +import useInfiniteProposals from '../../hooks/useInfiniteProposals' +import { UseProposalsFilter } from '../../hooks/useProposals' +import Empty from '../Common/Empty' +import FullWidthButton from '../Common/FullWidthButton' +import SkeletonBars from '../Common/SkeletonBars' +import Watermelon from '../Icon/Watermelon' + +import ProposalCreatedItem from './ProposalCreatedItem' + +interface Props { + proposalsFilter: Partial + pendingCoauthorRequests?: CoauthorAttributes[] + emptyDescriptionText: string + showCoauthoring?: boolean +} + +export function ProposalCreatedList({ + proposalsFilter, + pendingCoauthorRequests, + emptyDescriptionText, + showCoauthoring = false, +}: Props) { + const t = useFormatMessage() + const { proposals, isLoadingProposals, isFetchingNextPage, isFetchingProposals, hasMoreProposals, loadMore } = + useInfiniteProposals(proposalsFilter) + const hasProposals = proposals && proposals?.[0]?.total > 0 + + return ( + <> + {isLoadingProposals && } + {!isLoadingProposals && ( + <> + {hasProposals ? ( + proposals.map((page) => + page.data.map((proposal) => ( + req.proposal_id === proposal.id)} + /> + )) + ) + ) : ( + } description={emptyDescriptionText} /> + )} + {hasMoreProposals && ( + + {t('page.profile.activity.button')} + + )} + + )} + + ) +} diff --git a/src/components/Profile/ProposalsCreatedTab.tsx b/src/components/Profile/ProposalsCreatedTab.tsx index 323fd88be..d6548fcaf 100644 --- a/src/components/Profile/ProposalsCreatedTab.tsx +++ b/src/components/Profile/ProposalsCreatedTab.tsx @@ -2,13 +2,8 @@ import useAuthContext from 'decentraland-gatsby/dist/context/Auth/useAuthContext import { isSameAddress } from '../../entities/Snapshot/utils' import useFormatMessage from '../../hooks/useFormatMessage' -import usePaginatedProposals from '../../hooks/usePaginatedProposals' -import Empty from '../Common/Empty' -import FullWidthButton from '../Common/FullWidthButton' -import SkeletonBars from '../Common/SkeletonBars' -import Watermelon from '../Icon/Watermelon' -import ProposalCreatedItem from './ProposalCreatedItem' +import { ProposalCreatedList } from './ProposalCreatedList' interface Props { address?: string @@ -21,33 +16,18 @@ const ProposalsCreatedTab = ({ address }: Props) => { const isLoggedUserAddress = isSameAddress(account, address || '') const user = isLoggedUserAddress ? account : address - const { proposals, hasMoreProposals, loadMore, isLoadingProposals } = usePaginatedProposals({ - load: !!user, - ...(!!user && { user: user?.toLowerCase() }), - }) - const emptyDescriptionKey = isLoggedUserAddress ? 'page.profile.activity.my_proposals.empty' : 'page.profile.created_proposals.empty' return ( - <> - {isLoadingProposals && } - {!isLoadingProposals && ( - <> - {proposals.length > 0 ? ( - proposals.map((proposal) => ( - - )) - ) : ( - } description={t(emptyDescriptionKey)} /> - )} - - )} - {!isLoadingProposals && hasMoreProposals && ( - {t('page.profile.activity.button')} - )} - + ) } diff --git a/src/components/Profile/WatchlistTab.tsx b/src/components/Profile/WatchlistTab.tsx index afe35b1bd..3c2538488 100644 --- a/src/components/Profile/WatchlistTab.tsx +++ b/src/components/Profile/WatchlistTab.tsx @@ -1,43 +1,21 @@ import useAuthContext from 'decentraland-gatsby/dist/context/Auth/useAuthContext' import useFormatMessage from '../../hooks/useFormatMessage' -import usePaginatedProposals from '../../hooks/usePaginatedProposals' -import Empty from '../Common/Empty' -import FullWidthButton from '../Common/FullWidthButton' -import SkeletonBars from '../Common/SkeletonBars' -import Watermelon from '../Icon/Watermelon' -import ProposalCreatedItem from './ProposalCreatedItem' +import { ProposalCreatedList } from './ProposalCreatedList' const WatchlistTab = () => { const [account] = useAuthContext() const t = useFormatMessage() - const { proposals, hasMoreProposals, loadMore, isLoadingProposals } = usePaginatedProposals({ - load: !!account, - ...(!!account && { subscribed: account }), - }) - return ( - <> - {isLoadingProposals && } - {!isLoadingProposals && ( - <> - {proposals.length > 0 ? ( - proposals.map((proposal) => ) - ) : ( - } - description={t('page.profile.activity.watchlist.empty')} - /> - )} - - )} - {!isLoadingProposals && hasMoreProposals && ( - {t('page.profile.activity.button')} - )} - + ) } diff --git a/src/hooks/useInfiniteProposals.ts b/src/hooks/useInfiniteProposals.ts new file mode 100644 index 000000000..3c96d09f5 --- /dev/null +++ b/src/hooks/useInfiniteProposals.ts @@ -0,0 +1,34 @@ +import { useInfiniteQuery } from '@tanstack/react-query' + +import { DEFAULT_QUERY_STALE_TIME } from './constants' +import { UseProposalsFilter, getProposalsQueryFn } from './useProposals' + +const DEFAULT_ITEMS_PER_PAGE = 5 + +export default function useInfiniteProposals(filter: Partial = {}) { + const { + data: proposals, + isLoading: isLoadingProposals, + isFetching: isFetchingProposals, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useInfiniteQuery({ + queryKey: [`infinite-proposals#${JSON.stringify(filter)}`], + queryFn: getProposalsQueryFn(filter, DEFAULT_ITEMS_PER_PAGE), + staleTime: DEFAULT_QUERY_STALE_TIME, + getNextPageParam: (lastPage, pages) => { + const fetchedTotal = pages.reduce((acc, currentValue) => acc + currentValue.data.length, 0) + return lastPage.total > fetchedTotal ? pages.length : undefined + }, + }) + + return { + proposals: proposals?.pages, + isLoadingProposals, + isFetchingProposals, + isFetchingNextPage, + hasMoreProposals: !!hasNextPage, + loadMore: fetchNextPage, + } +} diff --git a/src/hooks/usePaginatedProposals.ts b/src/hooks/usePaginatedProposals.ts deleted file mode 100644 index 294ac3b4c..000000000 --- a/src/hooks/usePaginatedProposals.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useState } from 'react' - -import { ProposalAttributes } from '../entities/Proposal/types' - -import useProposals, { UseProposalsFilter } from './useProposals' - -const DEFAULT_ITEMS_PER_PAGE = 5 - -export default function usePaginatedProposals(filter: Partial = {}) { - const [page, setPage] = useState(1) - const [paginatedProposals, setPaginatedProposals] = useState([]) - const { proposals, isLoadingProposals } = useProposals({ - itemsPerPage: filter.itemsPerPage || DEFAULT_ITEMS_PER_PAGE, - ...filter, - page, - }) - - useEffect(() => { - if (proposals) { - setPaginatedProposals((prev) => [...prev, ...proposals.data]) - } - }, [proposals]) - - return { - proposals: paginatedProposals, - isLoadingProposals, - hasMoreProposals: paginatedProposals.length !== proposals?.total, - loadMore: () => setPage((prev) => prev + 1), - } -} diff --git a/src/hooks/useProposals.ts b/src/hooks/useProposals.ts index 901a2d2cc..03fffa9d9 100644 --- a/src/hooks/useProposals.ts +++ b/src/hooks/useProposals.ts @@ -5,6 +5,62 @@ import { MAX_PROPOSAL_LIMIT } from '../entities/Proposal/utils' import { DEFAULT_QUERY_STALE_TIME } from './constants' +export function getProposalsQueryFn( + filter: Partial = {}, + defaultItemsPerPage = MAX_PROPOSAL_LIMIT +) { + return async ({ pageParam = 0 }) => { + if (filter.load === false) { + return { + ok: true, + total: 0, + data: [], + } + } + const limit = filter.itemsPerPage ?? defaultItemsPerPage + const offset = ((filter.page ?? 1) - 1 || pageParam) * limit + const params: Partial = { limit, offset } + if (filter.status) { + params.status = filter.status + } + if (filter.type) { + params.type = filter.type + } + if (filter.subtype) { + params.subtype = filter.subtype + } + if (filter.user) { + params.user = filter.user + } + if (filter.subscribed) { + params.subscribed = !!filter.subscribed + } + if (filter.search) { + params.search = filter.search + } + if (filter.timeFrame) { + params.timeFrame = filter.timeFrame + } + if (filter.timeFrameKey) { + params.timeFrameKey = filter.timeFrameKey + } + if (filter.order) { + params.order = filter.order + } + if (filter.coauthor) { + params.coauthor = filter.coauthor + } + if (filter.snapshotIds) { + params.snapshotIds = filter.snapshotIds + } + if (filter.linkedProposalId) { + params.linkedProposalId = filter.linkedProposalId + } + + return Governance.get().getProposals({ ...params, limit, offset }) + } +} + export type UseProposalsFilter = Omit & { subscribed: string | boolean page: number @@ -15,56 +71,7 @@ export type UseProposalsFilter = Omit = {}) { const { data: proposals, isLoading: isLoadingProposals } = useQuery({ queryKey: [`proposals#${JSON.stringify(filter)}`], - queryFn: async () => { - if (filter.load === false) { - return { - ok: true, - total: 0, - data: [], - } - } - const limit = filter.itemsPerPage ?? MAX_PROPOSAL_LIMIT - const offset = ((filter.page ?? 1) - 1) * limit - const params: Partial = { limit, offset } - if (filter.status) { - params.status = filter.status - } - if (filter.type) { - params.type = filter.type - } - if (filter.subtype) { - params.subtype = filter.subtype - } - if (filter.user) { - params.user = filter.user - } - if (filter.subscribed) { - params.subscribed = !!filter.subscribed - } - if (filter.search) { - params.search = filter.search - } - if (filter.timeFrame) { - params.timeFrame = filter.timeFrame - } - if (filter.timeFrameKey) { - params.timeFrameKey = filter.timeFrameKey - } - if (filter.order) { - params.order = filter.order - } - if (filter.coauthor) { - params.coauthor = filter.coauthor - } - if (filter.snapshotIds) { - params.snapshotIds = filter.snapshotIds - } - if (filter.linkedProposalId) { - params.linkedProposalId = filter.linkedProposalId - } - - return Governance.get().getProposals({ ...params, limit, offset }) - }, + queryFn: getProposalsQueryFn(filter), staleTime: DEFAULT_QUERY_STALE_TIME, }) diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 3de5be407..d8b49d529 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -37,6 +37,7 @@ export default function ProfilePage() { if (!hasAddress) { navigate(`/profile/?address=${userAddress}`, { replace: true }) } + const { displayableAddress } = useProfile(address) const { delegation, isDelegationLoading, scores, isLoadingScores, vpDistribution, isLoadingVpDistribution } = useVotingPowerInformation(address)