Skip to content

Commit

Permalink
feat: group pagination (liftedinit#92)
Browse files Browse the repository at this point in the history
Co-authored-by: Felix C. Morency <[email protected]>
  • Loading branch information
chalabi2 and fmorency authored Nov 27, 2024
1 parent ef0a56a commit 54274d7
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 3 deletions.
83 changes: 83 additions & 0 deletions components/groups/components/__tests__/myGroups.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ mock.module('@/hooks/useQueries', () => ({
}),
}));

mock.module('@/hooks/useIsMobile', () => ({
__esModule: true,
default: jest.fn().mockReturnValue(false),
}));

const mockProps = {
groups: {
groups: [
Expand All @@ -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();
Expand Down Expand Up @@ -86,4 +108,65 @@ describe('YourGroups Component', () => {
{ shallow: true }
);
});

describe('Pagination', () => {
test('renders pagination controls when there are more items than page size', () => {
renderWithChainProvider(<YourGroups {...mockPropsWithManyGroups} />);

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(<YourGroups {...mockPropsWithManyGroups} />);

// 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(<YourGroups {...mockPropsWithManyGroups} />);

const prevButton = screen.getByRole('button', { name: /previous page/i });
expect(prevButton).toBeDisabled();
});

test('next button is disabled on last page', () => {
renderWithChainProvider(<YourGroups {...mockPropsWithManyGroups} />);

// 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(<YourGroups {...mockPropsWithManyGroups} />);

// 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(<YourGroups {...mockPropsWithManyGroups} />);

// 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);
});
});
});
86 changes: 83 additions & 3 deletions components/groups/components/myGroups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -134,7 +146,7 @@ export function YourGroups({
</thead>
<tbody className="space-y-4" role="rowgroup">
{isLoading
? Array(10)
? Array(8)
.fill(0)
.map((_, index) => (
<tr key={index} data-testid="skeleton-row">
Expand Down Expand Up @@ -174,12 +186,14 @@ export function YourGroups({
</td>
</tr>
))
: filteredGroups.map((group, index) => (
: paginatedGroups.map((group, index) => (
<GroupRow
key={index}
group={group}
proposals={
group.policies && group.policies.length > 0
group.policies &&
group.policies.length > 0 &&
proposals[group.policies[0].address]
? proposals[group.policies[0].address]
: []
}
Expand All @@ -188,6 +202,70 @@ export function YourGroups({
))}
</tbody>
</table>
{totalPages > 1 && (
<div
className="flex items-center justify-center gap-2"
onClick={e => e.stopPropagation()}
role="navigation"
aria-label="Pagination"
>
<button
onClick={e => {
e.stopPropagation();
setCurrentPage(prev => Math.max(1, prev - 1));
}}
disabled={currentPage === 1 || isLoading}
className="p-2 hover:bg-[#FFFFFF1A] rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Previous page"
>
</button>

{[...Array(totalPages)].map((_, index) => {
const pageNum = index + 1;
if (
pageNum === 1 ||
pageNum === totalPages ||
(pageNum >= currentPage - 1 && pageNum <= currentPage + 1)
) {
return (
<button
key={pageNum}
onClick={e => {
e.stopPropagation();
setCurrentPage(pageNum);
}}
className={`w-8 h-8 flex items-center justify-center rounded-lg transition-colors
${currentPage === pageNum ? 'bg-[#FFFFFF1A] text-white' : 'hover:bg-[#FFFFFF1A]'}`}
aria-label={`Page ${pageNum}`}
aria-current={currentPage === pageNum ? 'page' : undefined}
>
{pageNum}
</button>
);
} else if (pageNum === currentPage - 2 || pageNum === currentPage + 2) {
return (
<span key={pageNum} aria-hidden="true">
...
</span>
);
}
return null;
})}

<button
onClick={e => {
e.stopPropagation();
setCurrentPage(prev => Math.min(totalPages, prev + 1));
}}
disabled={currentPage === totalPages || isLoading}
className="p-2 hover:bg-[#FFFFFF1A] rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Next page"
>
</button>
</div>
)}
</div>
</div>
<div className="mt-6 w-full justify-center md:hidden block">
Expand Down Expand Up @@ -247,6 +325,8 @@ export function YourGroups({
/>
</React.Fragment>
))}

{/* Add pagination controls */}
</div>
);
}
Expand Down
1 change: 1 addition & 0 deletions hooks/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './usePoaLcdQueryClient';
export * from './useQueries';
export * from './useTx';
export * from './useContacts';
export { default as useIsMobile } from './useIsMobile';
29 changes: 29 additions & 0 deletions hooks/useIsMobile.ts
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 54274d7

Please sign in to comment.