From 252a95615dd7c0e62e614677b5f41a0cab30a039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20M=C4=83rgineanu?= Date: Thu, 5 Dec 2024 15:50:04 +0200 Subject: [PATCH] Added new components. --- src/UI/Pagination/Pagination.tsx | 202 + .../PaginationEdgeButton.tsx | 60 + .../components/PaginationEdgeButton/index.ts | 1 + .../paginationEdgeButtonStyles.scss | 53 + src/UI/Pagination/components/index.ts | 1 + .../getPagination/getPagination.spec.ts | 94 + .../helpers/getPagination/getPagination.ts | 69 + .../Pagination/helpers/getPagination/index.ts | 1 + src/UI/Pagination/helpers/index.ts | 1 + src/UI/Pagination/index.ts | 1 + src/UI/Pagination/paginationStyles.scss | 243 + src/UI/index.ts | 1 + .../AddressTable/AddressTable.tsx | 76 +- .../AddressTable/addressTableStyles.scss | 24 +- .../LedgerLoginContent/LedgerLoginContent.tsx | 2 + .../LedgerLoginContentBody.tsx | 3 + src/hooks/login/useAddressScreens.ts | 6 + src/hooks/login/useLedgerLogin.ts | 10 +- yarn.lock | 6725 +++++++++-------- 19 files changed, 4171 insertions(+), 3402 deletions(-) create mode 100644 src/UI/Pagination/Pagination.tsx create mode 100644 src/UI/Pagination/components/PaginationEdgeButton/PaginationEdgeButton.tsx create mode 100644 src/UI/Pagination/components/PaginationEdgeButton/index.ts create mode 100644 src/UI/Pagination/components/PaginationEdgeButton/paginationEdgeButtonStyles.scss create mode 100644 src/UI/Pagination/components/index.ts create mode 100644 src/UI/Pagination/helpers/getPagination/getPagination.spec.ts create mode 100644 src/UI/Pagination/helpers/getPagination/getPagination.ts create mode 100644 src/UI/Pagination/helpers/getPagination/index.ts create mode 100644 src/UI/Pagination/helpers/index.ts create mode 100644 src/UI/Pagination/index.ts create mode 100644 src/UI/Pagination/paginationStyles.scss diff --git a/src/UI/Pagination/Pagination.tsx b/src/UI/Pagination/Pagination.tsx new file mode 100644 index 000000000..dfa7d5877 --- /dev/null +++ b/src/UI/Pagination/Pagination.tsx @@ -0,0 +1,202 @@ +import React, { MouseEvent, useEffect, useState } from 'react'; +import { + faAngleLeft, + faAngleRight, + faAnglesLeft, + faAnglesRight +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import BigNumber from 'bignumber.js'; +import classNames from 'classnames'; + +import { DataTestIdsEnum } from 'constants/index'; +import { WithStylesImportType } from 'hocs/useStyles'; +import { withStyles } from 'hocs/withStyles'; +import { WithClassnameType } from 'UI/types'; +import { stringIsInteger } from 'utils'; + +import { PaginationEdgeButton } from './components'; +import { getPagination } from './helpers'; + +export interface PaginationPropsType + extends WithStylesImportType, + WithClassnameType { + onPageChange: (page: number) => void; + currentPage: number; + totalPages: number; + isDisabled?: boolean; + showLabels?: boolean; + showEdgeButtons?: boolean; + disabledClassName?: string; + buttonsClassNames?: string; +} + +const PaginationComponent = ({ + currentPage = 1, + totalPages, + className, + disabledClassName, + buttonsClassNames, + onPageChange, + isDisabled, + showLabels, + showEdgeButtons = true, + styles +}: PaginationPropsType) => { + const [currentPageIndex, setCurrentPageIndex] = useState(currentPage); + + const isLeftToggleDisabled = currentPageIndex === 1; + const isRightToggleDisabled = currentPageIndex === totalPages; + + const optionalDisabledClassName = disabledClassName + ? { [disabledClassName]: isDisabled } + : {}; + + const paginationItems = getPagination({ + currentPage: currentPageIndex, + totalPages + }); + + const handlePageClick = (newPageIndex: number) => { + if (newPageIndex === currentPageIndex) { + return; + } + + setCurrentPageIndex(newPageIndex); + onPageChange(newPageIndex); + }; + + const handlePaginationItemClick = (paginationItem: string) => { + if (stringIsInteger(paginationItem)) { + handlePageClick(new BigNumber(paginationItem).toNumber()); + } + }; + + const handleEdgePageClick = + (pageToNavigateTo: number) => (event: MouseEvent) => { + event.preventDefault(); + handlePageClick(pageToNavigateTo); + }; + + const isPaginationItemInTheHundreds = (paginationItem: string) => + stringIsInteger(paginationItem) && + new BigNumber(paginationItem).isGreaterThanOrEqualTo(100); + + const isCurrentPageActive = (paginationItem: string) => + new BigNumber(paginationItem).isEqualTo(currentPageIndex); + + useEffect(() => { + if (currentPage !== currentPageIndex) { + setCurrentPageIndex(currentPage); + } + }, [currentPage, currentPageIndex]); + + if (totalPages === 1) { + return null; + } + + return ( +
+ {showEdgeButtons && ( + + + + )} + + + +
+ {paginationItems.map((paginationItem, paginationItemIndex) => ( +
+ {stringIsInteger(paginationItem) ? ( +
handlePaginationItemClick(paginationItem)} + className={classNames( + styles?.paginationItem, + buttonsClassNames, + { [styles?.active]: isCurrentPageActive(paginationItem) }, + { [styles?.ellipsis]: !stringIsInteger(paginationItem) }, + { [styles?.disabled]: isDisabled }, + { + [styles?.hundreds]: + isPaginationItemInTheHundreds(paginationItem) + }, + optionalDisabledClassName + )} + > + + {paginationItem} + +
+ ) : ( + + {paginationItem} + + )} +
+ ))} +
+ + + + {showEdgeButtons && ( + + + + )} +
+ ); +}; + +export const Pagination = withStyles(PaginationComponent, { + ssrStyles: () => import('UI/Pagination/paginationStyles.scss'), + clientStyles: () => require('UI/Pagination/paginationStyles.scss').default +}); diff --git a/src/UI/Pagination/components/PaginationEdgeButton/PaginationEdgeButton.tsx b/src/UI/Pagination/components/PaginationEdgeButton/PaginationEdgeButton.tsx new file mode 100644 index 000000000..8910dedde --- /dev/null +++ b/src/UI/Pagination/components/PaginationEdgeButton/PaginationEdgeButton.tsx @@ -0,0 +1,60 @@ +import React, { MouseEvent } from 'react'; +import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import classNames from 'classnames'; + +import { WithStylesImportType } from 'hocs/useStyles'; +import { withStyles } from 'hocs/withStyles'; +import { WithClassnameType } from 'UI/types'; + +interface PaginationEdgeButtonPropsType + extends WithClassnameType, + WithStylesImportType { + label: string; + isInactive: boolean; + showLabels?: boolean; + paginationButtonIcon: IconDefinition; + onClick: (event: MouseEvent) => void; +} + +const PaginationEdgeButtonComponent = ({ + label, + onClick, + showLabels, + isInactive, + paginationButtonIcon, + className, + 'data-testid': dataTestId, + styles +}: PaginationEdgeButtonPropsType) => ( +
+ + + + {label} + +
+); + +export const PaginationEdgeButton = withStyles(PaginationEdgeButtonComponent, { + ssrStyles: () => + import( + 'UI/Pagination/components/PaginationEdgeButton/paginationEdgeButtonStyles.scss' + ), + clientStyles: () => + require('UI/Pagination/components/PaginationEdgeButton/paginationEdgeButtonStyles.scss') + .default +}); diff --git a/src/UI/Pagination/components/PaginationEdgeButton/index.ts b/src/UI/Pagination/components/PaginationEdgeButton/index.ts new file mode 100644 index 000000000..891c226ae --- /dev/null +++ b/src/UI/Pagination/components/PaginationEdgeButton/index.ts @@ -0,0 +1 @@ +export * from './PaginationEdgeButton'; diff --git a/src/UI/Pagination/components/PaginationEdgeButton/paginationEdgeButtonStyles.scss b/src/UI/Pagination/components/PaginationEdgeButton/paginationEdgeButtonStyles.scss new file mode 100644 index 000000000..144d81d36 --- /dev/null +++ b/src/UI/Pagination/components/PaginationEdgeButton/paginationEdgeButtonStyles.scss @@ -0,0 +1,53 @@ +.pagination-edge-button { + display: none; + gap: 8px; + align-items: center; + color: #e5e5e5; + transition: all 200ms ease; + cursor: pointer; + + @media (min-width: 576px) { + padding: 0; + margin: 0 4px; + display: flex; + } + + &.inactive { + color: #737373; + pointer-events: none; + + .pagination-edge-button-icon { + color: #737373; + } + } + + &:hover { + color: #23f7dd; + + .pagination-edge-button-icon { + color: #23f7dd; + } + } + + .pagination-edge-button-text { + display: none; + + &.show { + display: block; + } + + @media (min-width: 576px) { + display: block; + } + } + + .pagination-edge-button-icon { + transition: all 200ms ease; + color: #e5e5e5; + font-size: 12px; + + @media (min-width: 375px) { + font-size: 16px; + } + } +} diff --git a/src/UI/Pagination/components/index.ts b/src/UI/Pagination/components/index.ts new file mode 100644 index 000000000..891c226ae --- /dev/null +++ b/src/UI/Pagination/components/index.ts @@ -0,0 +1 @@ +export * from './PaginationEdgeButton'; diff --git a/src/UI/Pagination/helpers/getPagination/getPagination.spec.ts b/src/UI/Pagination/helpers/getPagination/getPagination.spec.ts new file mode 100644 index 000000000..5dbc2b4de --- /dev/null +++ b/src/UI/Pagination/helpers/getPagination/getPagination.spec.ts @@ -0,0 +1,94 @@ +import { ELLIPSIS } from 'constants/index'; + +import { getPagination } from '../getPagination'; + +describe('Pagination control function.', () => { + const stringifyPaginationItems = ( + paginateItems: PaginationItemType[] + ) => paginateItems.map((paginationItem) => String(paginationItem)); + + test('Watching page 1 of 2.', () => { + const expectedResult = stringifyPaginationItems([1, 2]); + const actualResult = getPagination({ currentPage: 1, totalPages: 2 }); + + expect(actualResult).toStrictEqual(expectedResult); + }); + + test('Watching page 1 of 7.', () => { + const expectedResult = stringifyPaginationItems([1, 2, 3, 4, 5, 6, 7]); + const actualResult = getPagination({ currentPage: 1, totalPages: 7 }); + + expect(actualResult).toStrictEqual(expectedResult); + }); + + test('Watching page 7 of 7.', () => { + const expectedResult = stringifyPaginationItems([1, 2, 3, 4, 5, 6, 7]); + const actualResult = getPagination({ currentPage: 7, totalPages: 7 }); + + expect(actualResult).toStrictEqual(expectedResult); + }); + + test('Watching page 1 of 59.', () => { + const expectedResultArray = [1, 2, 3, ELLIPSIS, 57, 58, 59]; + const expectedResult = stringifyPaginationItems(expectedResultArray); + const actualResult = getPagination({ currentPage: 1, totalPages: 59 }); + + expect(actualResult).toStrictEqual(expectedResult); + }); + + test('Watching page 3 of 59.', () => { + const expectedResultArray = [1, 2, 3, 4, ELLIPSIS, 58, 59]; + const expectedResult = stringifyPaginationItems(expectedResultArray); + const actualResult = getPagination({ currentPage: 3, totalPages: 59 }); + + expect(actualResult).toStrictEqual(expectedResult); + }); + + test('Watching page 4 of 569.', () => { + const expectedResultArray = [1, 2, 3, 4, 5, ELLIPSIS, 569]; + const expectedResult = stringifyPaginationItems(expectedResultArray); + const actualResult = getPagination({ currentPage: 4, totalPages: 569 }); + + expect(actualResult).toStrictEqual(expectedResult); + }); + + test('Watching page 444 of 569.', () => { + const expectedResultArray = [1, ELLIPSIS, 443, 444, 445, ELLIPSIS, 569]; + const expectedResult = stringifyPaginationItems(expectedResultArray); + const actualResult = getPagination({ currentPage: 444, totalPages: 569 }); + + expect(actualResult).toStrictEqual(expectedResult); + }); + + test('Watching page 666 of 669.', () => { + const expectedResultArray = [1, ELLIPSIS, 665, 666, 667, 668, 669]; + const expectedResult = stringifyPaginationItems(expectedResultArray); + const actualResult = getPagination({ currentPage: 666, totalPages: 669 }); + + expect(actualResult).toStrictEqual(expectedResult); + }); + + test('Watching page 1548 of 1550.', () => { + const expectedResultArray = [1, 2, ELLIPSIS, 1547, 1548, 1549, 1550]; + const expectedResult = stringifyPaginationItems(expectedResultArray); + const actualResult = getPagination({ currentPage: 1548, totalPages: 1550 }); + + expect(actualResult).toStrictEqual(expectedResult); + }); + + test('Watching page 2004 of 2005.', () => { + const expectedResultArray = [1, 2, 3, ELLIPSIS, 2003, 2004, 2005]; + const expectedResult = stringifyPaginationItems(expectedResultArray); + const actualResult = getPagination({ currentPage: 2004, totalPages: 2005 }); + + expect(actualResult).toStrictEqual(expectedResult); + }); + + test('Watching page 2014 of 2014.', () => { + const expectedResultArray = [1, 2, 3, ELLIPSIS, 2012, 2013, 2014]; + const expectedResult = stringifyPaginationItems(expectedResultArray); + const actualResult = getPagination({ currentPage: 2014, totalPages: 2014 }); + + expect(actualResult).toStrictEqual(expectedResult); + }); +}); diff --git a/src/UI/Pagination/helpers/getPagination/getPagination.ts b/src/UI/Pagination/helpers/getPagination/getPagination.ts new file mode 100644 index 000000000..974483e2c --- /dev/null +++ b/src/UI/Pagination/helpers/getPagination/getPagination.ts @@ -0,0 +1,69 @@ +import inRange from 'lodash/inRange'; +import range from 'lodash/range'; + +import { ELLIPSIS } from 'constants/index'; + +export interface GetPaginationType { + currentPage: number; + totalPages: number; +} + +export const getPagination = ({ + currentPage, + totalPages +}: GetPaginationType) => { + const maxSlots = 7; + const previousPage = currentPage - 1; + const nextPage = currentPage + 1; + const minBatchLength = 3; + const maxBatchLength = 5; + + if (totalPages <= maxSlots) { + return range(1, totalPages + 1).map((paginationItem) => + String(paginationItem) + ); + } + + const trimBatch = (batch: number[], comparableBatch: number[]) => + batch.includes(currentPage) + ? batch + : batch.slice(0, maxSlots - comparableBatch.length - 1); + + const isLeftBatchInRange = inRange( + nextPage - 1, + minBatchLength, + maxBatchLength + ); + + const isRightBatchInRange = inRange( + previousPage + 1, + totalPages - minBatchLength, + totalPages - 1 + ); + + const leftBatch = isLeftBatchInRange + ? range(1, nextPage + 1) + : range(1, maxBatchLength - 1); + + const rightBatch = isRightBatchInRange + ? range(previousPage, totalPages + 1) + : range(totalPages - minBatchLength + 1, totalPages + 1); + + const trimmedLeftBatch = trimBatch(leftBatch, rightBatch); + const trimmedRightBatch = trimBatch(rightBatch.reverse(), leftBatch); + const mergedEdgeBatches = trimmedLeftBatch.concat(trimmedRightBatch); + const middleBatch = [ELLIPSIS, previousPage, currentPage, nextPage, ELLIPSIS]; + + const [firstLeftBatchItem] = trimmedLeftBatch; + const [firstRightBatchItem] = trimmedRightBatch; + + const paginationItems = mergedEdgeBatches.includes(currentPage) + ? [...trimmedLeftBatch, ELLIPSIS, ...trimmedRightBatch.reverse()] + : [firstLeftBatchItem, ...middleBatch, firstRightBatchItem]; + + const paginationItemsAsStrings = paginationItems.map((paginationItem) => + String(paginationItem) + ); + + return paginationItemsAsStrings; +}; diff --git a/src/UI/Pagination/helpers/getPagination/index.ts b/src/UI/Pagination/helpers/getPagination/index.ts new file mode 100644 index 000000000..2d7f3268c --- /dev/null +++ b/src/UI/Pagination/helpers/getPagination/index.ts @@ -0,0 +1 @@ +export * from './getPagination'; diff --git a/src/UI/Pagination/helpers/index.ts b/src/UI/Pagination/helpers/index.ts new file mode 100644 index 000000000..2d7f3268c --- /dev/null +++ b/src/UI/Pagination/helpers/index.ts @@ -0,0 +1 @@ +export * from './getPagination'; diff --git a/src/UI/Pagination/index.ts b/src/UI/Pagination/index.ts new file mode 100644 index 000000000..e016c96b7 --- /dev/null +++ b/src/UI/Pagination/index.ts @@ -0,0 +1 @@ +export * from './Pagination'; diff --git a/src/UI/Pagination/paginationStyles.scss b/src/UI/Pagination/paginationStyles.scss new file mode 100644 index 000000000..48a0c3e35 --- /dev/null +++ b/src/UI/Pagination/paginationStyles.scss @@ -0,0 +1,243 @@ +.pagination { + display: flex; + align-items: center; + gap: 8px; + user-select: none; + font-family: 'Roobert Regular', sans-serif; + line-height: 1; + justify-content: center; + font-size: 16px; + max-width: 480px; + + @media (min-width: 768px) { + gap: 16px; + } + + .pagination-angle { + cursor: pointer; + transition: all 200ms ease; + display: none; + + @media (min-width: 576px) { + display: flex; + } + + &.disabled { + color: #737373; + pointer-events: none; + + .pagination-angle-icon { + color: #737373; + } + } + + &:hover { + color: #23f7dd; + + .pagination-angle-icon { + color: #23f7dd; + } + } + + .pagination-angle-icon { + color: #e5e5e5; + font-size: 12px; + + @media (min-width: 375px) { + font-size: 16px; + } + } + } + + .pagination-edge-button { + margin: 0; + + &.disabled { + pointer-events: none; + } + + &.reversed { + flex-direction: row-reverse; + } + } + + .pagination-items { + display: flex; + gap: 8px; + margin: 0 4px; + align-items: center; + + .pagination-item-wrapper { + cursor: pointer; + text-align: center; + height: calc(24px + 4px); + width: calc(24px + 4px); + display: flex; + align-items: center; + justify-content: center; + + @media (min-width: 768px) { + height: 32px; + width: 32px; + } + + .pagination-item { + color: #e5e5e5; + transition: color 0.2s ease-out; + position: relative; + border-radius: 4px; + text-align: center; + padding: 8px 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + height: calc(24px + 4px); + width: calc(24px + 4px); + + @media (min-width: 768px) { + font-size: 16px; + height: 32px; + width: 32px; + } + + &:before { + content: ''; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + transition: all 200ms ease; + background-color: #0e0e0e; + border-radius: 50%; + height: calc(24px + 4px); + width: calc(24px + 4px); + pointer-events: none; + z-index: 1; + + @media (min-width: 768px) { + height: 32px; + width: 32px; + } + } + + &:hover { + color: #23f7dd; + + &:before { + background-color: #143736; + } + } + + &.hundreds { + font-size: 8px; + + @media (min-width: 768px) { + font-size: calc(20px / 2); + } + } + + &.active { + color: #23f7dd; + + &:before { + background-color: #143736; + } + } + + &.disabled { + pointer-events: none; + } + + &.ellipsis { + margin: 0; + + &:before { + background-color: transparent; + } + + &:hover:before { + background-color: #143736; + } + } + + .pagination-item-text { + position: relative; + z-index: 2; + } + } + + .pagination-item-tooltip { + position: relative; + z-index: 2; + + .react-tooltip.react-tooltip__show ~ .tooltip-trigger { + color: #23f7dd; + + &:before { + opacity: 1; + } + } + + .react-tooltip.react-tooltip__place-bottom { + padding-top: 20px; + + .tooltip-arrow { + top: 16px !important; + } + } + + .react-tooltip.react-tooltip__place-top { + padding-bottom: 20px; + + .tooltip-arrow { + bottom: 16px !important; + } + } + + .tooltip-content, + .tooltip-arrow { + background-color: #000000; + } + + .tooltip-trigger { + margin: 0; + + &:before { + content: ''; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + transition: all 200ms ease; + background-color: #143736; + opacity: 0; + border-radius: 50%; + height: calc(24px + 4px); + width: calc(24px + 4px); + pointer-events: none; + z-index: 1; + + @media (min-width: 768px) { + height: 32px; + width: 32px; + } + } + + &:hover { + color: #23f7dd; + + &:before { + opacity: 1; + } + } + } + + .pagination-item-tooltip-text { + position: relative; + z-index: 2; + } + } + } + } +} diff --git a/src/UI/index.ts b/src/UI/index.ts index 0ffc3d91e..fec8bb687 100644 --- a/src/UI/index.ts +++ b/src/UI/index.ts @@ -20,6 +20,7 @@ export * from './TransactionsToastList'; export * from './TransactionsToastList/components'; export * from './Trim'; export * from './Loader'; +export * from './Pagination'; export * from './UsdValue'; export * from './walletConnect/WalletConnectLoginButton'; export * from './walletConnect/WalletConnectLoginContainer'; diff --git a/src/UI/ledger/LedgerLoginContainer/AddressTable/AddressTable.tsx b/src/UI/ledger/LedgerLoginContainer/AddressTable/AddressTable.tsx index ecf8b0609..0d7e8f767 100644 --- a/src/UI/ledger/LedgerLoginContainer/AddressTable/AddressTable.tsx +++ b/src/UI/ledger/LedgerLoginContainer/AddressTable/AddressTable.tsx @@ -1,19 +1,18 @@ import React, { ReactNode, useEffect, useState } from 'react'; -import { - faChevronLeft, - faChevronRight -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; + import { DataTestIdsEnum } from 'constants/index'; import { withStyles, WithStylesImportType } from 'hocs/withStyles'; +import { Pagination } from 'UI/Pagination'; import { getAccountBalance } from 'utils/account/getAccountBalance'; + import { WithClassnameType } from '../../../types'; import { AddressRow } from '../AddressRow'; import { LedgerColumnsEnum } from '../enums'; import { LedgerLoading } from '../LedgerLoading'; const ADDRESSES_PER_PAGE = 10; +const TOTAL_ADDRESSES_PAGES = 500; export interface AddressTablePropsType extends WithClassnameType { accounts: string[]; @@ -32,6 +31,7 @@ export interface AddressTablePropsType extends WithClassnameType { dataTestId?: string; loading: boolean; onConfirmSelectedAddress: () => void; + onGoToSpecificPage: (page: number) => void; onGoToNextPage: () => void; onGoToPrevPage: () => void; onSelectAddress: (address: { address: string; index: number } | null) => void; @@ -47,6 +47,7 @@ const AddressTableComponent = ({ dataTestId = DataTestIdsEnum.addressTableContainer, loading, onConfirmSelectedAddress, + onGoToSpecificPage, onGoToNextPage, onGoToPrevPage, onSelectAddress, @@ -125,6 +126,20 @@ const AddressTableComponent = ({ onConfirmSelectedAddress(); }; + const handlePageChange = (newPage: number) => { + if (newPage - 1 === startIndex + 1) { + onGoToNextPage(); + return; + } + + if (newPage - 1 === startIndex - 1) { + onGoToPrevPage(); + return; + } + + onGoToSpecificPage(newPage - 1); + }; + const columns = [ LedgerColumnsEnum.Address, LedgerColumnsEnum.Balance, @@ -195,49 +210,14 @@ const AddressTableComponent = ({
-
- - - -
+