Skip to content

Commit

Permalink
Create pagination on home page proposals list
Browse files Browse the repository at this point in the history
  • Loading branch information
adamgall committed Jan 8, 2025
1 parent 09aee92 commit 9d837c4
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 15 deletions.
67 changes: 59 additions & 8 deletions src/components/DaoDashboard/Activities/ProposalsHome.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Box, Flex, Icon, Show, Button } from '@chakra-ui/react';
import { CaretDown, Funnel } from '@phosphor-icons/react';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState, Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { DAO_ROUTES } from '../../../constants/routes';
import { useProposalsSortedAndFiltered } from '../../../hooks/DAO/proposal/useProposals';
import { useCanUserCreateProposal } from '../../../hooks/utils/useCanUserSubmitProposal';
import { usePagination } from '../../../hooks/utils/usePagination';
import { useFractal } from '../../../providers/App/AppProvider';
import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore';
import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore';
Expand All @@ -21,6 +22,7 @@ import { CreateProposalMenu } from '../../ui/menus/CreateProposalMenu';
import { OptionMenu } from '../../ui/menus/OptionMenu';
import { ModalType } from '../../ui/modals/ModalProvider';
import { useDecentModal } from '../../ui/modals/useDecentModal';
import { PaginationControls } from '../../ui/utils/PaginationControls';
import { Sort } from '../../ui/utils/Sort';
import { ActivityFreeze } from './ActivityFreeze';

Expand All @@ -36,6 +38,17 @@ export function ProposalsHome() {

const { proposals, getProposalsTotal } = useProposalsSortedAndFiltered({ sortBy, filters });

const { currentPage, setCurrentPage, pageSize, setPageSize, totalPages, getPaginatedItems } =
usePagination({
totalItems: proposals.length,
});

// Calculate paginated proposals
const paginatedProposals = useMemo(
() => getPaginatedItems(proposals),
[proposals, getPaginatedItems],
);

const { governance, guardContracts } = useFractal();
const { safe } = useDaoInfoStore();

Expand Down Expand Up @@ -81,7 +94,6 @@ export function ProposalsHome() {
FractalProposalState.ACTIVE,
FractalProposalState.EXECUTABLE,
FractalProposalState.EXECUTED,

FractalProposalState.REJECTED,
];

Expand All @@ -91,7 +103,6 @@ export function ProposalsHome() {
FractalProposalState.TIMELOCKED,
FractalProposalState.EXECUTABLE,
FractalProposalState.EXECUTED,

FractalProposalState.REJECTED,
FractalProposalState.EXPIRED,
];
Expand Down Expand Up @@ -129,6 +140,7 @@ export function ProposalsHome() {
return [...prevState, filter];
}
});
setCurrentPage(1);
};

const filterOptions = allOptions.map(state => ({
Expand All @@ -138,12 +150,35 @@ export function ProposalsHome() {
isSelected: filters.includes(state),
}));

const handleSortChange: Dispatch<SetStateAction<SortBy>> = value => {
if (typeof value === 'function') {
setSortBy(prev => {
const newValue = value(prev);
setCurrentPage(1);
return newValue;
});
} else {
setSortBy(value);
setCurrentPage(1);
}
};

const handleSelectAll = () => {
setFilters(allOptions);
setCurrentPage(1);
};

const handleClearFilters = () => {
setFilters([]);
setCurrentPage(1);
};

const filterTitle =
filters.length === 1
? t(filters[0])
: filters.length === allOptions.length
? t('filterProposalsAllSelected')
: filters.length === 0 // No filters selected means no filtering applied
: filters.length === 0
? t('filterProposalsNoneSelected')
: t('filterProposalsNSelected', { count: filters.length });

Expand Down Expand Up @@ -234,15 +269,15 @@ export function ProposalsHome() {
variant="tertiary"
size="sm"
mt="0.5rem"
onClick={() => setFilters(allOptions)}
onClick={handleSelectAll}
>
{t('selectAll', { ns: 'common' })}
</Button>
<Button
variant="tertiary"
size="sm"
mt="0.5rem"
onClick={() => setFilters([])}
onClick={handleClearFilters}
>
{t('clear', { ns: 'common' })}
</Button>
Expand All @@ -252,7 +287,7 @@ export function ProposalsHome() {

<Sort
sortBy={sortBy}
setSortBy={setSortBy}
setSortBy={handleSortChange}
buttonProps={{ disabled: !proposals.length }}
/>
</Flex>
Expand All @@ -277,7 +312,23 @@ export function ProposalsHome() {
</Show>
</Flex>

<ProposalsList proposals={proposals} />
<ProposalsList proposals={paginatedProposals} />

{/* PAGINATION CONTROLS */}
{proposals.length > 0 && (
<Flex
justify="flex-end"
mx="0.5rem"
>
<PaginationControls
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
pageSize={pageSize}
onPageSizeChange={setPageSize}
/>
</Flex>
)}
</Flex>
</Box>
);
Expand Down
16 changes: 10 additions & 6 deletions src/components/Proposals/ProposalsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import NoDataCard from '../ui/containers/NoDataCard';
import { InfoBoxLoader } from '../ui/loaders/InfoBoxLoader';
import ProposalCard from './ProposalCard/ProposalCard';

export function ProposalsList({ proposals }: { proposals: FractalProposal[] }) {
interface ProposalsListProps {
proposals: FractalProposal[];
}

export function ProposalsList({ proposals }: ProposalsListProps) {
const {
governance: { type, loadingProposals, allProposalsLoaded },
} = useFractal();
Expand All @@ -22,15 +26,15 @@ export function ProposalsList({ proposals }: { proposals: FractalProposal[] }) {
<InfoBoxLoader />
</Box>
) : proposals.length > 0 ? (
[
...proposals.map(proposal => (
<>
{proposals.map(proposal => (
<ProposalCard
key={proposal.proposalId}
proposal={proposal}
/>
)),
!allProposalsLoaded && <InfoBoxLoader />,
]
))}
{!allProposalsLoaded && <InfoBoxLoader />}
</>
) : (
<NoDataCard
emptyText="emptyProposals"
Expand Down
142 changes: 142 additions & 0 deletions src/components/ui/utils/PaginationControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {
Button,
Flex,
HStack,
Icon,
Menu,
MenuButton,
MenuItem,
MenuList,
Text,
} from '@chakra-ui/react';
import {
CaretDoubleLeft,
CaretDoubleRight,
CaretDown,
CaretLeft,
CaretRight,
} from '@phosphor-icons/react';
import { ComponentType } from 'react';
import { useTranslation } from 'react-i18next';
import { NEUTRAL_2_82_TRANSPARENT } from '../../../constants/common';
import { PAGE_SIZE_OPTIONS } from '../../../hooks/utils/usePagination';
import { EaseOutComponent } from './EaseOutComponent';

interface NavButtonProps {
onClick: () => void;
isDisabled: boolean;
icon: ComponentType;
}

function NavButton({ onClick, isDisabled, icon: IconComponent }: NavButtonProps) {
return (
<Button
onClick={onClick}
isDisabled={isDisabled}
variant="tertiary"
size="sm"
>
<IconComponent />
</Button>
);
}

interface PaginationControlsProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
pageSize: number;
onPageSizeChange: (size: number) => void;
}

export function PaginationControls({
currentPage,
totalPages,
onPageChange,
pageSize,
onPageSizeChange,
}: PaginationControlsProps) {
const { t } = useTranslation(['common']);

return (
<Flex
align="center"
gap={2}
>
<Menu isLazy>
<MenuButton
as={Button}
variant="tertiary"
size="sm"
>
<Flex alignItems="center">
<Text fontSize="sm">{pageSize}</Text>
<Icon
ml="0.25rem"
as={CaretDown}
/>
</Flex>
</MenuButton>
<MenuList
borderWidth="1px"
borderColor="neutral-3"
borderRadius="0.75rem"
bg={NEUTRAL_2_82_TRANSPARENT}
backdropFilter="auto"
backdropBlur="10px"
minWidth="min-content"
zIndex={5}
p="0.25rem"
>
<EaseOutComponent>
{PAGE_SIZE_OPTIONS.map(size => (
<MenuItem
key={size}
borderRadius="0.75rem"
p="0.5rem 0.5rem"
sx={{
'&:hover': { bg: 'neutral-3' },
}}
onClick={() => onPageSizeChange(size)}
>
<Text fontSize="sm">{size}</Text>
</MenuItem>
))}
</EaseOutComponent>
</MenuList>
</Menu>

<HStack spacing={1}>
<NavButton
onClick={() => onPageChange(1)}
isDisabled={currentPage === 1}
icon={CaretDoubleLeft}
/>
<NavButton
onClick={() => onPageChange(currentPage - 1)}
isDisabled={currentPage === 1}
icon={CaretLeft}
/>

<Text
fontSize="sm"
px={2}
color="lilac-0"
>
{t('pageXofY', { current: currentPage, total: totalPages })}
</Text>

<NavButton
onClick={() => onPageChange(currentPage + 1)}
isDisabled={currentPage === totalPages}
icon={CaretRight}
/>
<NavButton
onClick={() => onPageChange(totalPages)}
isDisabled={currentPage === totalPages}
icon={CaretDoubleRight}
/>
</HStack>
</Flex>
);
}
61 changes: 61 additions & 0 deletions src/hooks/utils/usePagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';

// Only exported for PaginationControls component
export const PAGE_SIZE_OPTIONS = [5, 10, 25, 50, 100];
const DEFAULT_PAGE_SIZE = 10;

interface UsePaginationProps {
totalItems: number;
}

const QUERY_PARAMS = {
PAGE: 'page',
SIZE: 'size',
} as const;

export function usePagination({ totalItems }: UsePaginationProps) {
const [searchParams, setSearchParams] = useSearchParams();

const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get(QUERY_PARAMS.PAGE);
return page ? parseInt(page) : 1;
});

const [pageSize, setPageSize] = useState(() => {
const size = searchParams.get(QUERY_PARAMS.SIZE);
return size && PAGE_SIZE_OPTIONS.includes(parseInt(size)) ? parseInt(size) : DEFAULT_PAGE_SIZE;
});

// Calculate total pages
const totalPages = Math.ceil(totalItems / pageSize);

// Update URL when state changes
useEffect(() => {
const newParams = new URLSearchParams(searchParams);
newParams.set(QUERY_PARAMS.PAGE, currentPage.toString());
newParams.set(QUERY_PARAMS.SIZE, pageSize.toString());
setSearchParams(newParams, { replace: true });
}, [currentPage, pageSize, searchParams, setSearchParams]);

// Handle page size changes
const handlePageSizeChange = (newSize: number) => {
setPageSize(newSize);
setCurrentPage(1);
};

// Calculate paginated items
const getPaginatedItems = <T>(items: T[]) => {
const startIndex = (currentPage - 1) * pageSize;
return items.slice(startIndex, startIndex + pageSize);
};

return {
currentPage,
setCurrentPage,
pageSize,
setPageSize: handlePageSizeChange,
totalPages,
getPaginatedItems,
};
}
3 changes: 2 additions & 1 deletion src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,6 @@
"automaticChainSwitchingErrorMessage": "We couldn't automatically switch to the DAO's network. Please try to switch networks in your connected wallet.",
"and": "and",
"days": "Days",
"owner": "Owner"
"owner": "Owner",
"pageXofY": "Page {{current}} of {{total}}"
}

0 comments on commit 9d837c4

Please sign in to comment.