Repository: NASA-IMPACT/veda-ui
Pull request #1500: feat: Add pagination to catalog content
Author: login: AliceR
Created: 2025-03-10T11:58:34Z | Merged: 2025-03-18T11:56:16Z

**Related Ticket:** Close #1454

### Description of Changes
Adding the pagination to the data catalog content. This implementation is using client side filtering to limit the number of cards rendered from the list provided. 

### Notes & Questions About Changes
The items per page are now hardcoded - should there be a way to control this number via UI? Otherwise, how do we decide on a good number?
The current page is not yet synced with the url, though this may be expected?

### Validation / Testing
Ensure all expected cards are listed on `/data-catalog` - play with the value `itemsPerPage` (i.e. reduce to 3) to see the effects of pagination. Click previous, next, and a selected page.


Linked issue #1454: Add pagination from USWDS
Add Pagination component from USWDS. (NOTE: We might need a separate ticket to hook up this pagination logic up or we could do it in this ticket).

USWDS Pagination: https://trussworks.github.io/react-uswds/?path=/docs/components-pagination--docs

![Image](https://github.com/user-attachments/assets/364428f0-80df-47c4-8c29-5dedaab65caf)
Linked issue comments: user: login: AliceR
created_at: 2025-03-12T11:55:17Z
body: [#1500](https://github.com/NASA-IMPACT/veda-ui/pull/1500) is still in review
author_association: MEMBER

Diff:
diff --git a/app/scripts/components/common/catalog/catalog-content.tsx b/app/scripts/components/common/catalog/catalog-content.tsx
index 673cefeb5..c04b3ac58 100644
--- a/app/scripts/components/common/catalog/catalog-content.tsx
+++ b/app/scripts/components/common/catalog/catalog-content.tsx
@@ -6,7 +6,6 @@ import { CollecticonDatasetLayers } from '../icons/dataset-layers';
 import { USWDSCardGroup } from '../uswds';
 import { Card, CardType } from '../card';
 import { Tags } from '../tags';
-import { createCardLabel } from '../card/uswds-cards/utils';
 import { prepareDatasets } from './prepare-datasets';
 import FiltersControl from './filters-control';
 import { CatalogCard } from './catalog-card';
@@ -30,7 +29,7 @@ import {
 import { OptionItem } from '$components/common/form/checkable-filter';
 import { Pill } from '$styles/pill';
 import { usePreviousValue } from '$utils/use-effect-previous';
-
+import Pagination from '$components/common/pagination';
 import { useVedaUI } from '$context/veda-ui-provider';
 import { findParentDatasetFromLayer } from '$utils/data-utils';
 
@@ -46,6 +45,7 @@ export interface CatalogContentProps {
   search: string;
   taxonomies: Record<string, string[]>;
   onAction: (action: FilterActions, value?: any) => void;
+  itemsPerPage?: number;
 }
 
 const DEFAULT_SORT_OPTION = 'asc';
@@ -59,11 +59,15 @@ function CatalogContent({
   emptyStateContent,
   search,
   taxonomies,
-  onAction
+  onAction,
+  itemsPerPage = 7
 }: CatalogContentProps) {
   const [exclusiveSourceSelected, setExclusiveSourceSelected] = useState<
     string | null
   >(null);
+
+  const [currentPage, setCurrentPage] = useState(1);
+
   const isSelectable = selectedIds !== undefined;
   const {
     routes: { dataCatalogPath }
@@ -234,29 +238,33 @@ function CatalogContent({
   const generateCardsWithRoute = useMemo(
     () => (
       <USWDSCardGroup style={{ gap: '40px' }}>
-        {datasetsToDisplay.map((d) => {
-          const imgSrc = getMediaProperty(undefined, d, 'src');
-          const imgAlt = getMediaProperty(undefined, d, 'alt');
-          const description = getDatasetDescription(undefined, d);
-          const allTaxonomyValues = getAllTaxonomyValues(d).map((v) => v.name);
-
-          return (
-            <Card
-              cardType={CardType.FLAG}
-              key={d.id}
-              imgSrc={imgSrc}
-              imgAlt={imgAlt}
-              title={d.name}
-              description={description}
-              footerContent={<Tags items={allTaxonomyValues} />}
-              to={`${dataCatalogPath}/${d.id}`}
-              cardLabel='data_collection'
-            />
-          );
-        })}
+        {datasetsToDisplay
+          .slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
+          .map((d) => {
+            const imgSrc = getMediaProperty(undefined, d, 'src');
+            const imgAlt = getMediaProperty(undefined, d, 'alt');
+            const description = getDatasetDescription(undefined, d);
+            const allTaxonomyValues = getAllTaxonomyValues(d).map(
+              (v) => v.name
+            );
+
+            return (
+              <Card
+                cardType={CardType.FLAG}
+                key={d.id}
+                imgSrc={imgSrc}
+                imgAlt={imgAlt}
+                title={d.name}
+                description={description}
+                footerContent={<Tags items={allTaxonomyValues} />}
+                to={`${dataCatalogPath}/${d.id}`}
+                cardLabel='data_collection'
+              />
+            );
+          })}
       </USWDSCardGroup>
     ),
-    [datasetsToDisplay, dataCatalogPath]
+    [currentPage, itemsPerPage, datasetsToDisplay, dataCatalogPath]
   );
 
   useEffect(() => {
@@ -276,6 +284,8 @@ function CatalogContent({
       .length;
   };
 
+  const totalPages = Math.ceil(datasetsToDisplay.length / itemsPerPage);
+
   return (
     <Content>
       <FiltersControl
@@ -298,59 +308,74 @@ function CatalogContent({
           handleClearTags={handleClearTags}
         />
         {datasetsToDisplay.length ? (
-          isSelectable ? (
-            <Cards>
-              {datasetsToDisplay.map((currentDataset) => (
-                <div key={currentDataset.id}>
-                  <div>
-                    <Headline>
-                      <ParentDatasetTitle>
-                        <CollecticonDatasetLayers /> {currentDataset.name}
-                        {getSelectedLayerCount(currentDataset) > 0 && (
-                          <SelectedCard>
-                            <span>
-                              {getSelectedLayerCount(currentDataset)} selected
-                            </span>
-                          </SelectedCard>
-                        )}
-                      </ParentDatasetTitle>
-                      {currentDataset.sourceExclusive && (
-                        <WarningPill variation='warning'>
-                          {EXCLUSIVE_SOURCE_WARNING}
-                        </WarningPill>
-                      )}
-                    </Headline>
-                    <Paragraph>
-                      <TextHighlight
-                        value={search}
-                        disabled={search.length < 3}
-                      >
-                        {currentDataset.description}
-                      </TextHighlight>
-                    </Paragraph>
-                  </div>
-                  <Cards>
-                    {currentDataset.layers.map((datasetLayer) => (
-                      <li key={datasetLayer.id}>
-                        <CatalogCard
-                          searchTerm={search}
-                          layer={datasetLayer}
-                          dataset={currentDataset}
-                          selectable={true}
-                          selected={selectedIds.includes(datasetLayer.id)}
-                          onDatasetClick={() => {
-                            onCardSelect(datasetLayer.id, currentDataset);
-                          }}
-                        />
-                      </li>
-                    ))}
-                  </Cards>
-                </div>
-              ))}
-            </Cards>
-          ) : (
-            generateCardsWithRoute
-          )
+          <>
+            {isSelectable ? (
+              <Cards>
+                {datasetsToDisplay
+                  .slice(
+                    (currentPage - 1) * itemsPerPage,
+                    currentPage * itemsPerPage
+                  )
+                  .map((currentDataset) => (
+                    <div key={currentDataset.id}>
+                      <div>
+                        <Headline>
+                          <ParentDatasetTitle>
+                            <CollecticonDatasetLayers /> {currentDataset.name}
+                            {getSelectedLayerCount(currentDataset) > 0 && (
+                              <SelectedCard>
+                                <span>
+                                  {getSelectedLayerCount(currentDataset)}{' '}
+                                  selected
+                                </span>
+                              </SelectedCard>
+                            )}
+                          </ParentDatasetTitle>
+                          {currentDataset.sourceExclusive && (
+                            <WarningPill variation='warning'>
+                              {EXCLUSIVE_SOURCE_WARNING}
+                            </WarningPill>
+                          )}
+                        </Headline>
+                        <Paragraph>
+                          <TextHighlight
+                            value={search}
+                            disabled={search.length < 3}
+                          >
+                            {currentDataset.description}
+                          </TextHighlight>
+                        </Paragraph>
+                      </div>
+                      <Cards>
+                        {currentDataset.layers.map((datasetLayer) => (
+                          <li key={datasetLayer.id}>
+                            <CatalogCard
+                              searchTerm={search}
+                              layer={datasetLayer}
+                              dataset={currentDataset}
+                              selectable={true}
+                              selected={selectedIds.includes(datasetLayer.id)}
+                              onDatasetClick={() => {
+                                onCardSelect(datasetLayer.id, currentDataset);
+                              }}
+                            />
+                          </li>
+                        ))}
+                      </Cards>
+                    </div>
+                  ))}
+              </Cards>
+            ) : (
+              generateCardsWithRoute
+            )}
+            {totalPages > 1 && (
+              <Pagination
+                currentPage={currentPage}
+                setCurrentPage={setCurrentPage}
+                totalPages={totalPages}
+              />
+            )}
+          </>
         ) : (
           <EmptyState>
             {emptyStateContent ?? (
diff --git a/app/scripts/components/common/pagination/index.tsx b/app/scripts/components/common/pagination/index.tsx
new file mode 100644
index 000000000..aff6911c8
--- /dev/null
+++ b/app/scripts/components/common/pagination/index.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+
+import { Pagination } from './pagination';
+import { PaginationContainer } from './pagination-container';
+
+export default function WrappedPagination({
+  currentPage,
+  setCurrentPage,
+  totalPages
+}: {
+  currentPage: number;
+  setCurrentPage: (currentPage: number) => void;
+  totalPages: number;
+}) {
+  return (
+    <PaginationContainer {...{ currentPage, setCurrentPage }}>
+      {(props) => <Pagination {...props} totalPages={totalPages} />}
+    </PaginationContainer>
+  );
+}
diff --git a/app/scripts/components/common/pagination/pagination-container.tsx b/app/scripts/components/common/pagination/pagination-container.tsx
new file mode 100644
index 000000000..7aa781f77
--- /dev/null
+++ b/app/scripts/components/common/pagination/pagination-container.tsx
@@ -0,0 +1,20 @@
+export function PaginationContainer({ currentPage, setCurrentPage, children }) {
+  const onClickNext = () => {
+    setCurrentPage(currentPage + 1);
+  };
+
+  const onClickPrevious = () => {
+    setCurrentPage(currentPage - 1);
+  };
+
+  const onClickPageNumber = (pageNumber) => {
+    setCurrentPage(pageNumber);
+  };
+
+  return children({
+    currentPage,
+    onClickNext,
+    onClickPageNumber,
+    onClickPrevious
+  });
+}
diff --git a/app/scripts/components/common/pagination/pagination.scss b/app/scripts/components/common/pagination/pagination.scss
new file mode 100644
index 000000000..0a59e271a
--- /dev/null
+++ b/app/scripts/components/common/pagination/pagination.scss
@@ -0,0 +1,13 @@
+/**
+ * For some reason, the pagination button loses its bottom border when hovered, 
+ * because the button on hover has border-bottom set to 0.
+ * It works fine in the official USWDS component, but it's the same behavior in 
+ * the react-uswds variant. 
+ * This overwrites the border style only for this component.
+ */
+
+.usa-pagination__button:hover,
+.usa-pagination__button:focus,
+.usa-pagination__button:active {
+  border-bottom: 1px solid;
+}
diff --git a/app/scripts/components/common/pagination/pagination.tsx b/app/scripts/components/common/pagination/pagination.tsx
new file mode 100644
index 000000000..6e51e017f
--- /dev/null
+++ b/app/scripts/components/common/pagination/pagination.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { USWDSPagination } from '$uswds';
+
+interface PaginationProps {
+  currentPage: number;
+  onClickNext: () => void;
+  onClickPageNumber: (pageNumber: number) => void;
+  onClickPrevious: () => void;
+  totalPages: number;
+}
+
+export const Pagination = ({
+  currentPage,
+  onClickNext,
+  onClickPageNumber,
+  onClickPrevious,
+  totalPages
+}: PaginationProps) => {
+  return (
+    <USWDSPagination
+      currentPage={currentPage}
+      maxSlots={7}
+      totalPages={totalPages}
+      onClickNext={onClickNext}
+      onClickPageNumber={(event) => {
+        const target = event.target as HTMLElement;
+        const pageNumber = parseInt(target.innerText);
+        onClickPageNumber(pageNumber);
+      }}
+      onClickPrevious={onClickPrevious}
+      pathname='/pagination'
+    />
+  );
+};
diff --git a/app/scripts/components/sandbox/pagination/pagination-container.tsx b/app/scripts/components/sandbox/pagination/pagination-container.tsx
index 976f97be9..fe4a82232 100644
--- a/app/scripts/components/sandbox/pagination/pagination-container.tsx
+++ b/app/scripts/components/sandbox/pagination/pagination-container.tsx
@@ -1,4 +1,3 @@
-'use client';
 import { useState } from 'react';
 
 export function PaginationContainer({ children }) {
diff --git a/app/scripts/styles/veda-components.scss b/app/scripts/styles/veda-components.scss
index e1b767aa5..ba851de73 100644
--- a/app/scripts/styles/veda-components.scss
+++ b/app/scripts/styles/veda-components.scss
@@ -1,7 +1,8 @@
 // Custom VEDA UI styles should be added here, so that they can be
 // picked up by Parcel and included in the final CSS bundle for the veda-ui library.
-@use "../components/common/page-header/page-header.scss";
-@use "../components/common/page-header/logo-container/logo-container.scss";
-@use "../components/common/banner/banner.scss";
-@use "../components/common/datepicker/datepicker.scss";
-@use "../components/common/page-footer/page-footer.scss"
+@use '../components/common/page-header/page-header.scss';
+@use '../components/common/page-header/logo-container/logo-container.scss';
+@use '../components/common/banner/banner.scss';
+@use '../components/common/datepicker/datepicker.scss';
+@use '../components/common/page-footer/page-footer.scss';
+@use '../components/common/pagination/pagination.scss';


Reviews:
user: login: dzole0311
body: Looks good!

I think persisting the state in the url would be great, but that can be a separate ticket.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T08:55:43Z
user: login: AliceR
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T09:10:12Z
user: login: AliceR
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T09:59:11Z
user: login: AliceR
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T10:03:17Z
user: login: dzole0311
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-11T10:47:06Z
user: login: sandrahoang686
body: We need to make sure when doing this work that everything works and looks correct in the NextJs instance as well, could you spin up a preview and link it to this PR? - something like https://github.com/NASA-IMPACT/next-veda-ui/pull/52/files.

We also should be tagging @faustoperez as a reviewer to these PRs for design purposes. That way we could iteratively review the DataCatalog Component as components are being added/migrated.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T14:03:48Z
user: login: faustoperez
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-12T11:37:15Z
user: login: AliceR
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T12:18:09Z
user: login: sandrahoang686
body: Linking this NextJs PR Alice created for deploy preview: 
https://github.com/NASA-IMPACT/next-veda-ui/pull/58

Validated on Nextjs side! @faustoperez if you'd like to review the template instance preview as well? Lgtm. Gjore has already done the code review so I will skip that.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-13T13:46:12Z
user: login: faustoperez
body: Looks and works great!
state: APPROVED
author_association: NONE
submitted_at: 2025-03-18T10:57:37Z

Review comments:
diff_hunk: @@ -258,6 +259,9 @@ function CatalogContent({
       .length;
   };
 
+  const [currentPage, setCurrentPage] = useState(1);
+  const itemsPerPage = 7; // QUESTION: Should this value be controlled by the user (UI)?
path: app/scripts/components/common/catalog/catalog-content.tsx
user: login: dzole0311
body: > // QUESTION: Should this value be controlled by the user (UI)?

That would be a nice option. The `7` can be a default, and we could additionally pass in `itemsPerPage` as an optional prop to the `CatalogContent`
created_at: 2025-03-11T08:32:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,34 @@
+import React from 'react';
+import { USWDSPagination } from '$uswds';
+
+interface PaginationProps {
+  currentPage: number;
+  onClickNext: () => void;
+  onClickPageNumber: (pageNumber: number) => void;
+  onClickPrevious: () => void;
+  totalPages: number;
+}
+
+export const Pagination = ({
+  currentPage,
+  onClickNext,
+  onClickPageNumber,
+  onClickPrevious,
+  totalPages
+}: PaginationProps) => {
+  return (
+    <USWDSPagination
path: app/scripts/components/common/pagination/pagination.tsx
user: login: dzole0311
body: I noticed that the pagination button loses its bottom border when hovered, because the button on hover has `border-bottom` set to 0. 

<img width="848" alt="Screenshot 2025-03-11 at 09 40 06" src="https://github.com/user-attachments/assets/9dfcfa12-e9f7-403a-b8d1-ec88c11adef8" />

It works fine in the [official USWDS component](https://designsystem.digital.gov/components/pagination/), but it's the same behavior in the [react-uswds](https://trussworks.github.io/react-uswds/?path=/docs/components-pagination--docs) variant. We can probably fix this by setting it to `border-bottom: 1px solid;` on hover only for this component.


created_at: 2025-03-11T08:48:18Z
author_association: COLLABORATOR
diff_hunk: @@ -280,65 +284,83 @@ function CatalogContent({
           handleClearTags={handleClearTags}
         />
         {datasetsToDisplay.length ? (
-          isSelectable ? (
-            <Cards>
-              {datasetsToDisplay.map((currentDataset) => (
-                <div key={currentDataset.id}>
-                  <div>
-                    <Headline>
-                      <ParentDatasetTitle>
-                        <CollecticonDatasetLayers /> {currentDataset.name}
-                        {getSelectedLayerCount(currentDataset) > 0 && (
-                          <SelectedCard>
-                            <span>
-                              {getSelectedLayerCount(currentDataset)} selected
-                            </span>
-                          </SelectedCard>
-                        )}
-                      </ParentDatasetTitle>
-                      {currentDataset.sourceExclusive && (
-                        <WarningPill variation='warning'>
-                          {EXCLUSIVE_SOURCE_WARNING}
-                        </WarningPill>
-                      )}
-                    </Headline>
-                    <Paragraph>
-                      <TextHighlight
-                        value={search}
-                        disabled={search.length < 3}
-                      >
-                        {currentDataset.description}
-                      </TextHighlight>
-                    </Paragraph>
-                  </div>
-                  <Cards>
-                    {currentDataset.layers.map((datasetLayer) => (
-                      <li key={datasetLayer.id}>
-                        <CatalogCard
-                          searchTerm={search}
-                          layer={datasetLayer}
-                          dataset={currentDataset}
-                          selectable={true}
-                          selected={selectedIds.includes(datasetLayer.id)}
-                          onDatasetClick={() => {
-                            onCardSelect(datasetLayer.id, currentDataset);
-                          }}
-                        />
-                      </li>
-                    ))}
-                  </Cards>
-                </div>
-              ))}
-            </Cards>
-          ) : (
-            <Cards>
-              {datasetsToDisplay.map((d) => (
-                <li key={d.id}>
-                  <CatalogCard dataset={d} searchTerm={search} />
-                </li>
-              ))}
-            </Cards>
-          )
+          <>
+            {isSelectable ? (
+              <Cards>
+                {datasetsToDisplay
+                  .slice(
+                    (currentPage - 1) * itemsPerPage,
+                    currentPage * itemsPerPage
+                  )
+                  .map((currentDataset) => (
+                    <div key={currentDataset.id}>
+                      <div>
+                        <Headline>
+                          <ParentDatasetTitle>
+                            <CollecticonDatasetLayers /> {currentDataset.name}
+                            {getSelectedLayerCount(currentDataset) > 0 && (
+                              <SelectedCard>
+                                <span>
+                                  {getSelectedLayerCount(currentDataset)}{' '}
+                                  selected
+                                </span>
+                              </SelectedCard>
+                            )}
+                          </ParentDatasetTitle>
+                          {currentDataset.sourceExclusive && (
+                            <WarningPill variation='warning'>
+                              {EXCLUSIVE_SOURCE_WARNING}
+                            </WarningPill>
+                          )}
+                        </Headline>
+                        <Paragraph>
+                          <TextHighlight
+                            value={search}
+                            disabled={search.length < 3}
+                          >
+                            {currentDataset.description}
+                          </TextHighlight>
+                        </Paragraph>
+                      </div>
+                      <Cards>
+                        {currentDataset.layers.map((datasetLayer) => (
+                          <li key={datasetLayer.id}>
+                            <CatalogCard
+                              searchTerm={search}
+                              layer={datasetLayer}
+                              dataset={currentDataset}
+                              selectable={true}
+                              selected={selectedIds.includes(datasetLayer.id)}
+                              onDatasetClick={() => {
+                                onCardSelect(datasetLayer.id, currentDataset);
+                              }}
+                            />
+                          </li>
+                        ))}
+                      </Cards>
+                    </div>
+                  ))}
+              </Cards>
+            ) : (
+              <Cards>
+                {datasetsToDisplay
+                  .slice(
+                    (currentPage - 1) * itemsPerPage,
+                    currentPage * itemsPerPage
+                  )
+                  .map((d) => (
+                    <li key={d.id}>
+                      <CatalogCard dataset={d} searchTerm={search} />
+                    </li>
+                  ))}
+              </Cards>
+            )}
+            <Pagination
path: app/scripts/components/common/catalog/catalog-content.tsx
user: login: dzole0311
body: Should we display pagination controls even when the total number of items is less than the `itemsPerPage` value? Currently the pagination is always visible, but it might be cleaner to hide it when unnecessary (e.g. when all items fit on a single page)?
created_at: 2025-03-11T08:55:13Z
author_association: COLLABORATOR
diff_hunk: @@ -280,65 +284,83 @@ function CatalogContent({
           handleClearTags={handleClearTags}
         />
         {datasetsToDisplay.length ? (
-          isSelectable ? (
-            <Cards>
-              {datasetsToDisplay.map((currentDataset) => (
-                <div key={currentDataset.id}>
-                  <div>
-                    <Headline>
-                      <ParentDatasetTitle>
-                        <CollecticonDatasetLayers /> {currentDataset.name}
-                        {getSelectedLayerCount(currentDataset) > 0 && (
-                          <SelectedCard>
-                            <span>
-                              {getSelectedLayerCount(currentDataset)} selected
-                            </span>
-                          </SelectedCard>
-                        )}
-                      </ParentDatasetTitle>
-                      {currentDataset.sourceExclusive && (
-                        <WarningPill variation='warning'>
-                          {EXCLUSIVE_SOURCE_WARNING}
-                        </WarningPill>
-                      )}
-                    </Headline>
-                    <Paragraph>
-                      <TextHighlight
-                        value={search}
-                        disabled={search.length < 3}
-                      >
-                        {currentDataset.description}
-                      </TextHighlight>
-                    </Paragraph>
-                  </div>
-                  <Cards>
-                    {currentDataset.layers.map((datasetLayer) => (
-                      <li key={datasetLayer.id}>
-                        <CatalogCard
-                          searchTerm={search}
-                          layer={datasetLayer}
-                          dataset={currentDataset}
-                          selectable={true}
-                          selected={selectedIds.includes(datasetLayer.id)}
-                          onDatasetClick={() => {
-                            onCardSelect(datasetLayer.id, currentDataset);
-                          }}
-                        />
-                      </li>
-                    ))}
-                  </Cards>
-                </div>
-              ))}
-            </Cards>
-          ) : (
-            <Cards>
-              {datasetsToDisplay.map((d) => (
-                <li key={d.id}>
-                  <CatalogCard dataset={d} searchTerm={search} />
-                </li>
-              ))}
-            </Cards>
-          )
+          <>
+            {isSelectable ? (
+              <Cards>
+                {datasetsToDisplay
+                  .slice(
+                    (currentPage - 1) * itemsPerPage,
+                    currentPage * itemsPerPage
+                  )
+                  .map((currentDataset) => (
+                    <div key={currentDataset.id}>
+                      <div>
+                        <Headline>
+                          <ParentDatasetTitle>
+                            <CollecticonDatasetLayers /> {currentDataset.name}
+                            {getSelectedLayerCount(currentDataset) > 0 && (
+                              <SelectedCard>
+                                <span>
+                                  {getSelectedLayerCount(currentDataset)}{' '}
+                                  selected
+                                </span>
+                              </SelectedCard>
+                            )}
+                          </ParentDatasetTitle>
+                          {currentDataset.sourceExclusive && (
+                            <WarningPill variation='warning'>
+                              {EXCLUSIVE_SOURCE_WARNING}
+                            </WarningPill>
+                          )}
+                        </Headline>
+                        <Paragraph>
+                          <TextHighlight
+                            value={search}
+                            disabled={search.length < 3}
+                          >
+                            {currentDataset.description}
+                          </TextHighlight>
+                        </Paragraph>
+                      </div>
+                      <Cards>
+                        {currentDataset.layers.map((datasetLayer) => (
+                          <li key={datasetLayer.id}>
+                            <CatalogCard
+                              searchTerm={search}
+                              layer={datasetLayer}
+                              dataset={currentDataset}
+                              selectable={true}
+                              selected={selectedIds.includes(datasetLayer.id)}
+                              onDatasetClick={() => {
+                                onCardSelect(datasetLayer.id, currentDataset);
+                              }}
+                            />
+                          </li>
+                        ))}
+                      </Cards>
+                    </div>
+                  ))}
+              </Cards>
+            ) : (
+              <Cards>
+                {datasetsToDisplay
+                  .slice(
+                    (currentPage - 1) * itemsPerPage,
+                    currentPage * itemsPerPage
+                  )
+                  .map((d) => (
+                    <li key={d.id}>
+                      <CatalogCard dataset={d} searchTerm={search} />
+                    </li>
+                  ))}
+              </Cards>
+            )}
+            <Pagination
path: app/scripts/components/common/catalog/catalog-content.tsx
user: login: AliceR
body: That is an interesting point. I would argue that for UI consistency it would be better to keep the boxed [1]. However, it depends on how often we actually have multi-page scenarios. If it’s almost always just a single page, hiding it would indeed be cleaner. 
@faustoperez , do you have an opinion about this specific situation?
created_at: 2025-03-11T09:10:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,34 @@
+import React from 'react';
+import { USWDSPagination } from '$uswds';
+
+interface PaginationProps {
+  currentPage: number;
+  onClickNext: () => void;
+  onClickPageNumber: (pageNumber: number) => void;
+  onClickPrevious: () => void;
+  totalPages: number;
+}
+
+export const Pagination = ({
+  currentPage,
+  onClickNext,
+  onClickPageNumber,
+  onClickPrevious,
+  totalPages
+}: PaginationProps) => {
+  return (
+    <USWDSPagination
path: app/scripts/components/common/pagination/pagination.tsx
user: login: AliceR
body: done: [07e08ba](https://github.com/NASA-IMPACT/veda-ui/pull/1500/commits/07e08babcea1960cc26365f3bf202c6b80f2304b)
created_at: 2025-03-11T09:59:11Z
author_association: MEMBER
diff_hunk: @@ -258,6 +259,9 @@ function CatalogContent({
       .length;
   };
 
+  const [currentPage, setCurrentPage] = useState(1);
+  const itemsPerPage = 7; // QUESTION: Should this value be controlled by the user (UI)?
path: app/scripts/components/common/catalog/catalog-content.tsx
user: login: AliceR
body: like this [aa0937b](https://github.com/NASA-IMPACT/veda-ui/pull/1500/commits/aa0937b84b77aada4896ac2c0781cce262dfd33b)?
created_at: 2025-03-11T10:03:17Z
author_association: MEMBER
diff_hunk: @@ -280,65 +284,83 @@ function CatalogContent({
           handleClearTags={handleClearTags}
         />
         {datasetsToDisplay.length ? (
-          isSelectable ? (
-            <Cards>
-              {datasetsToDisplay.map((currentDataset) => (
-                <div key={currentDataset.id}>
-                  <div>
-                    <Headline>
-                      <ParentDatasetTitle>
-                        <CollecticonDatasetLayers /> {currentDataset.name}
-                        {getSelectedLayerCount(currentDataset) > 0 && (
-                          <SelectedCard>
-                            <span>
-                              {getSelectedLayerCount(currentDataset)} selected
-                            </span>
-                          </SelectedCard>
-                        )}
-                      </ParentDatasetTitle>
-                      {currentDataset.sourceExclusive && (
-                        <WarningPill variation='warning'>
-                          {EXCLUSIVE_SOURCE_WARNING}
-                        </WarningPill>
-                      )}
-                    </Headline>
-                    <Paragraph>
-                      <TextHighlight
-                        value={search}
-                        disabled={search.length < 3}
-                      >
-                        {currentDataset.description}
-                      </TextHighlight>
-                    </Paragraph>
-                  </div>
-                  <Cards>
-                    {currentDataset.layers.map((datasetLayer) => (
-                      <li key={datasetLayer.id}>
-                        <CatalogCard
-                          searchTerm={search}
-                          layer={datasetLayer}
-                          dataset={currentDataset}
-                          selectable={true}
-                          selected={selectedIds.includes(datasetLayer.id)}
-                          onDatasetClick={() => {
-                            onCardSelect(datasetLayer.id, currentDataset);
-                          }}
-                        />
-                      </li>
-                    ))}
-                  </Cards>
-                </div>
-              ))}
-            </Cards>
-          ) : (
-            <Cards>
-              {datasetsToDisplay.map((d) => (
-                <li key={d.id}>
-                  <CatalogCard dataset={d} searchTerm={search} />
-                </li>
-              ))}
-            </Cards>
-          )
+          <>
+            {isSelectable ? (
+              <Cards>
+                {datasetsToDisplay
+                  .slice(
+                    (currentPage - 1) * itemsPerPage,
+                    currentPage * itemsPerPage
+                  )
+                  .map((currentDataset) => (
+                    <div key={currentDataset.id}>
+                      <div>
+                        <Headline>
+                          <ParentDatasetTitle>
+                            <CollecticonDatasetLayers /> {currentDataset.name}
+                            {getSelectedLayerCount(currentDataset) > 0 && (
+                              <SelectedCard>
+                                <span>
+                                  {getSelectedLayerCount(currentDataset)}{' '}
+                                  selected
+                                </span>
+                              </SelectedCard>
+                            )}
+                          </ParentDatasetTitle>
+                          {currentDataset.sourceExclusive && (
+                            <WarningPill variation='warning'>
+                              {EXCLUSIVE_SOURCE_WARNING}
+                            </WarningPill>
+                          )}
+                        </Headline>
+                        <Paragraph>
+                          <TextHighlight
+                            value={search}
+                            disabled={search.length < 3}
+                          >
+                            {currentDataset.description}
+                          </TextHighlight>
+                        </Paragraph>
+                      </div>
+                      <Cards>
+                        {currentDataset.layers.map((datasetLayer) => (
+                          <li key={datasetLayer.id}>
+                            <CatalogCard
+                              searchTerm={search}
+                              layer={datasetLayer}
+                              dataset={currentDataset}
+                              selectable={true}
+                              selected={selectedIds.includes(datasetLayer.id)}
+                              onDatasetClick={() => {
+                                onCardSelect(datasetLayer.id, currentDataset);
+                              }}
+                            />
+                          </li>
+                        ))}
+                      </Cards>
+                    </div>
+                  ))}
+              </Cards>
+            ) : (
+              <Cards>
+                {datasetsToDisplay
+                  .slice(
+                    (currentPage - 1) * itemsPerPage,
+                    currentPage * itemsPerPage
+                  )
+                  .map((d) => (
+                    <li key={d.id}>
+                      <CatalogCard dataset={d} searchTerm={search} />
+                    </li>
+                  ))}
+              </Cards>
+            )}
+            <Pagination
path: app/scripts/components/common/catalog/catalog-content.tsx
user: login: faustoperez
body: @AliceR I would say we don't show the pagination component unless it's needed 👍 
created_at: 2025-03-12T11:37:15Z
author_association: NONE
diff_hunk: @@ -280,65 +284,83 @@ function CatalogContent({
           handleClearTags={handleClearTags}
         />
         {datasetsToDisplay.length ? (
-          isSelectable ? (
-            <Cards>
-              {datasetsToDisplay.map((currentDataset) => (
-                <div key={currentDataset.id}>
-                  <div>
-                    <Headline>
-                      <ParentDatasetTitle>
-                        <CollecticonDatasetLayers /> {currentDataset.name}
-                        {getSelectedLayerCount(currentDataset) > 0 && (
-                          <SelectedCard>
-                            <span>
-                              {getSelectedLayerCount(currentDataset)} selected
-                            </span>
-                          </SelectedCard>
-                        )}
-                      </ParentDatasetTitle>
-                      {currentDataset.sourceExclusive && (
-                        <WarningPill variation='warning'>
-                          {EXCLUSIVE_SOURCE_WARNING}
-                        </WarningPill>
-                      )}
-                    </Headline>
-                    <Paragraph>
-                      <TextHighlight
-                        value={search}
-                        disabled={search.length < 3}
-                      >
-                        {currentDataset.description}
-                      </TextHighlight>
-                    </Paragraph>
-                  </div>
-                  <Cards>
-                    {currentDataset.layers.map((datasetLayer) => (
-                      <li key={datasetLayer.id}>
-                        <CatalogCard
-                          searchTerm={search}
-                          layer={datasetLayer}
-                          dataset={currentDataset}
-                          selectable={true}
-                          selected={selectedIds.includes(datasetLayer.id)}
-                          onDatasetClick={() => {
-                            onCardSelect(datasetLayer.id, currentDataset);
-                          }}
-                        />
-                      </li>
-                    ))}
-                  </Cards>
-                </div>
-              ))}
-            </Cards>
-          ) : (
-            <Cards>
-              {datasetsToDisplay.map((d) => (
-                <li key={d.id}>
-                  <CatalogCard dataset={d} searchTerm={search} />
-                </li>
-              ))}
-            </Cards>
-          )
+          <>
+            {isSelectable ? (
+              <Cards>
+                {datasetsToDisplay
+                  .slice(
+                    (currentPage - 1) * itemsPerPage,
+                    currentPage * itemsPerPage
+                  )
+                  .map((currentDataset) => (
+                    <div key={currentDataset.id}>
+                      <div>
+                        <Headline>
+                          <ParentDatasetTitle>
+                            <CollecticonDatasetLayers /> {currentDataset.name}
+                            {getSelectedLayerCount(currentDataset) > 0 && (
+                              <SelectedCard>
+                                <span>
+                                  {getSelectedLayerCount(currentDataset)}{' '}
+                                  selected
+                                </span>
+                              </SelectedCard>
+                            )}
+                          </ParentDatasetTitle>
+                          {currentDataset.sourceExclusive && (
+                            <WarningPill variation='warning'>
+                              {EXCLUSIVE_SOURCE_WARNING}
+                            </WarningPill>
+                          )}
+                        </Headline>
+                        <Paragraph>
+                          <TextHighlight
+                            value={search}
+                            disabled={search.length < 3}
+                          >
+                            {currentDataset.description}
+                          </TextHighlight>
+                        </Paragraph>
+                      </div>
+                      <Cards>
+                        {currentDataset.layers.map((datasetLayer) => (
+                          <li key={datasetLayer.id}>
+                            <CatalogCard
+                              searchTerm={search}
+                              layer={datasetLayer}
+                              dataset={currentDataset}
+                              selectable={true}
+                              selected={selectedIds.includes(datasetLayer.id)}
+                              onDatasetClick={() => {
+                                onCardSelect(datasetLayer.id, currentDataset);
+                              }}
+                            />
+                          </li>
+                        ))}
+                      </Cards>
+                    </div>
+                  ))}
+              </Cards>
+            ) : (
+              <Cards>
+                {datasetsToDisplay
+                  .slice(
+                    (currentPage - 1) * itemsPerPage,
+                    currentPage * itemsPerPage
+                  )
+                  .map((d) => (
+                    <li key={d.id}>
+                      <CatalogCard dataset={d} searchTerm={search} />
+                    </li>
+                  ))}
+              </Cards>
+            )}
+            <Pagination
path: app/scripts/components/common/catalog/catalog-content.tsx
user: login: AliceR
body: Removed conditionally here: [779cccc](https://github.com/NASA-IMPACT/veda-ui/pull/1500/commits/779ccccf7079b04bbb75b5d9a773689c7c7afc53)
created_at: 2025-03-12T12:18:09Z
author_association: MEMBER

PR comments:
user: login: netlify[bot]
created_at: 2025-03-10T11:58:51Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *veda-ui* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 24f28ae0854bc92dd301ef94d141f1ea47047ee7 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/veda-ui/deploys/67d2bb9f5bfca200086c0b72 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1500--veda-ui.netlify.app](https://deploy-preview-1500--veda-ui.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTE1MDAtLXZlZGEtdWkubmV0bGlmeS5hcHAifQ.UiRTp89anXC6KvuTvi-wKSLGt93ko86D8fiihAl9XQE)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [veda-ui Preview](https://deploy-preview-1500--veda-ui.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/veda-ui/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: AliceR
created_at: 2025-03-13T11:43:40Z
body: @faustoperez this is how the pagination would look like in the data-catalog: https://deploy-preview-1500--veda-ui.netlify.app/sandbox/pagination
We don't see it on the data catalog itself in the deploy preview, because there are not enough items for more than a single page, and we decided to hide the pagination in this case.

I used the default USWDS component with just our theme overwrites. I don't think there are any changes in particular to the pagination in your designs?
author_association: MEMBER
user: login: aboydnw
created_at: 2025-03-13T18:53:45Z
body: I created a ticket for changing number of items visible and for tracking page in url. I think that work can be deprioritized for now, but now we at least have an item to track. https://github.com/NASA-IMPACT/veda-ui/issues/1512
author_association: CONTRIBUTOR
user: login: faustoperez
created_at: 2025-03-18T10:57:06Z
body: Looking great @AliceR! I don't think we'll have more than 4-5 pages, so no need to make a distinction between the default and the unbounded behavior here https://designsystem.digital.gov/components/pagination/
author_association: NONE