From 54274d7af9c3d01db5b71c246a715d06debf37c5 Mon Sep 17 00:00:00 2001
From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com>
Date: Wed, 27 Nov 2024 08:56:34 -0700
Subject: [PATCH] feat: group pagination (#92)
Co-authored-by: Felix C. Morency <1102868+fmorency@users.noreply.github.com>
---
.../components/__tests__/myGroups.test.tsx | 83 ++++++++++++++++++
components/groups/components/myGroups.tsx | 86 ++++++++++++++++++-
hooks/index.tsx | 1 +
hooks/useIsMobile.ts | 29 +++++++
4 files changed, 196 insertions(+), 3 deletions(-)
create mode 100644 hooks/useIsMobile.ts
diff --git a/components/groups/components/__tests__/myGroups.test.tsx b/components/groups/components/__tests__/myGroups.test.tsx
index 30b5c245..2b94b379 100644
--- a/components/groups/components/__tests__/myGroups.test.tsx
+++ b/components/groups/components/__tests__/myGroups.test.tsx
@@ -31,6 +31,11 @@ mock.module('@/hooks/useQueries', () => ({
}),
}));
+mock.module('@/hooks/useIsMobile', () => ({
+ __esModule: true,
+ default: jest.fn().mockReturnValue(false),
+}));
+
const mockProps = {
groups: {
groups: [
@@ -48,6 +53,23 @@ const mockProps = {
isLoading: false,
};
+const mockPropsWithManyGroups = {
+ groups: {
+ groups: Array(12)
+ .fill(null)
+ .map((_, index) => ({
+ id: `${index + 1}`,
+ ipfsMetadata: { title: `Group ${index + 1}` },
+ policies: [{ address: `policy${index + 1}`, decision_policy: { threshold: '1' } }],
+ admin: `admin${index + 1}`,
+ members: [{ member: { address: `member${index + 1}` } }],
+ total_weight: '1',
+ })),
+ },
+ proposals: {},
+ isLoading: false,
+};
+
describe('YourGroups Component', () => {
afterEach(() => {
mock.restore();
@@ -86,4 +108,65 @@ describe('YourGroups Component', () => {
{ shallow: true }
);
});
+
+ describe('Pagination', () => {
+ test('renders pagination controls when there are more items than page size', () => {
+ renderWithChainProvider();
+
+ expect(screen.getByRole('navigation', { name: /pagination/i })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /next page/i })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /previous page/i })).toBeInTheDocument();
+ });
+
+ test('pagination controls navigate between pages correctly', () => {
+ renderWithChainProvider();
+
+ // Should start with page 1
+ expect(screen.getByRole('button', { name: 'Page 1', current: 'page' })).toBeInTheDocument();
+
+ // Click next page
+ fireEvent.click(screen.getByRole('button', { name: /next page/i }));
+ expect(screen.getByRole('button', { name: 'Page 2', current: 'page' })).toBeInTheDocument();
+
+ // Click previous page
+ fireEvent.click(screen.getByRole('button', { name: /previous page/i }));
+ expect(screen.getByRole('button', { name: 'Page 1', current: 'page' })).toBeInTheDocument();
+ });
+
+ test('previous button is disabled on first page', () => {
+ renderWithChainProvider();
+
+ const prevButton = screen.getByRole('button', { name: /previous page/i });
+ expect(prevButton).toBeDisabled();
+ });
+
+ test('next button is disabled on last page', () => {
+ renderWithChainProvider();
+
+ // Navigate to last page
+ const totalPages = Math.ceil(mockPropsWithManyGroups.groups.groups.length / 8);
+ for (let i = 1; i < totalPages; i++) {
+ fireEvent.click(screen.getByRole('button', { name: /next page/i }));
+ }
+
+ const nextButton = screen.getByRole('button', { name: /next page/i });
+ expect(nextButton).toBeDisabled();
+ });
+
+ test('direct page selection works correctly', () => {
+ renderWithChainProvider();
+
+ // Click page 2 button
+ fireEvent.click(screen.getByRole('button', { name: 'Page 2' }));
+ expect(screen.getByRole('button', { name: 'Page 2', current: 'page' })).toBeInTheDocument();
+ });
+
+ test('shows correct number of items per page', () => {
+ renderWithChainProvider();
+
+ // On desktop (non-mobile), should show 8 items per page
+ const groupRows = screen.getAllByRole('button', { name: /Select Group \d+ group/i });
+ expect(groupRows).toHaveLength(8);
+ });
+ });
});
diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx
index 93cd7a3c..6c9ae5da 100644
--- a/components/groups/components/myGroups.tsx
+++ b/components/groups/components/myGroups.tsx
@@ -18,6 +18,7 @@ import { PiInfo } from 'react-icons/pi';
import { GroupInfo } from '../modals/groupInfo';
import { MemberManagementModal } from '../modals/memberManagementModal';
import { useChain } from '@cosmos-kit/react';
+import useIsMobile from '@/hooks/useIsMobile';
export function YourGroups({
groups,
@@ -31,6 +32,11 @@ export function YourGroups({
refetch: () => void;
}) {
const [searchTerm, setSearchTerm] = useState('');
+ const [currentPage, setCurrentPage] = useState(1);
+ const isMobile = useIsMobile();
+
+ const pageSize = isMobile ? 6 : 8;
+
const [selectedGroup, setSelectedGroup] = useState<{
policyAddress: string;
name: string;
@@ -44,6 +50,12 @@ export function YourGroups({
(group.ipfsMetadata?.title || 'Untitled Group').toLowerCase().includes(searchTerm.toLowerCase())
);
+ const totalPages = Math.ceil(filteredGroups.length / pageSize);
+ const paginatedGroups = filteredGroups.slice(
+ (currentPage - 1) * pageSize,
+ currentPage * pageSize
+ );
+
useEffect(() => {
// Check if there's a policy address in the URL on component mount
const { policyAddress } = router.query;
@@ -134,7 +146,7 @@ export function YourGroups({
{isLoading
- ? Array(10)
+ ? Array(8)
.fill(0)
.map((_, index) => (
@@ -174,12 +186,14 @@ export function YourGroups({
))
- : filteredGroups.map((group, index) => (
+ : paginatedGroups.map((group, index) => (
0
+ group.policies &&
+ group.policies.length > 0 &&
+ proposals[group.policies[0].address]
? proposals[group.policies[0].address]
: []
}
@@ -188,6 +202,70 @@ export function YourGroups({
))}
+ {totalPages > 1 && (
+ e.stopPropagation()}
+ role="navigation"
+ aria-label="Pagination"
+ >
+
+
+ {[...Array(totalPages)].map((_, index) => {
+ const pageNum = index + 1;
+ if (
+ pageNum === 1 ||
+ pageNum === totalPages ||
+ (pageNum >= currentPage - 1 && pageNum <= currentPage + 1)
+ ) {
+ return (
+
+ );
+ } else if (pageNum === currentPage - 2 || pageNum === currentPage + 2) {
+ return (
+
+ ...
+
+ );
+ }
+ return null;
+ })}
+
+
+
+ )}
@@ -247,6 +325,8 @@ export function YourGroups({
/>
))}
+
+ {/* Add pagination controls */}
);
}
diff --git a/hooks/index.tsx b/hooks/index.tsx
index a575fa9e..c594bbcb 100644
--- a/hooks/index.tsx
+++ b/hooks/index.tsx
@@ -5,3 +5,4 @@ export * from './usePoaLcdQueryClient';
export * from './useQueries';
export * from './useTx';
export * from './useContacts';
+export { default as useIsMobile } from './useIsMobile';
diff --git a/hooks/useIsMobile.ts b/hooks/useIsMobile.ts
new file mode 100644
index 00000000..be8cbdfd
--- /dev/null
+++ b/hooks/useIsMobile.ts
@@ -0,0 +1,29 @@
+import { useState, useEffect } from 'react';
+
+const useIsMobile = (breakpoint: number = 1024) => {
+ const [isMobile, setIsMobile] = useState(false);
+
+ useEffect(() => {
+ // Only run on client side
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ const checkIsMobile = () => {
+ setIsMobile(window.innerWidth <= breakpoint);
+ };
+
+ // Initial check
+ checkIsMobile();
+
+ // Add event listener
+ window.addEventListener('resize', checkIsMobile);
+
+ // Cleanup
+ return () => window.removeEventListener('resize', checkIsMobile);
+ }, [breakpoint]);
+
+ return isMobile;
+};
+
+export default useIsMobile;