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)