Skip to content

Commit

Permalink
Use server actions for load more and pager views
Browse files Browse the repository at this point in the history
  • Loading branch information
pookmish committed May 23, 2024
1 parent 86cf5f4 commit e498768
Show file tree
Hide file tree
Showing 10 changed files with 491 additions and 395 deletions.
11 changes: 5 additions & 6 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import PageHeader from "@components/global/page-header";
import {Icon} from "next/dist/lib/metadata/types/metadata-types";
import {montserrat} from "../src/styles/fonts";
import DrupalWindowSync from "@components/elements/drupal-window-sync";
import {isPreviewMode} from "@lib/drupal/utils";
import UserAnalytics from "@components/elements/user-analytics";
import Editori11y from "@components/tools/editorially";

const appleIcons: Icon[] = [60, 72, 76, 114, 120, 144, 152, 180].map(size => ({
url: `https://www-media.stanford.edu/assets/favicon/apple-touch-icon-${size}x${size}.png`,
Expand Down Expand Up @@ -43,14 +43,13 @@ export const metadata = {
export const revalidate = false;

const RootLayout = ({children, modal}: { children: React.ReactNode, modal: React.ReactNode }) => {
const isPreview = isPreviewMode();
const isDevMode = process.env.NODE_ENV === "development";
return (
<html lang="en" className={montserrat.className}>
{/* Add Google Analytics and SiteImprove when not in preview mode. */}
{!isPreview &&
<UserAnalytics/>
}
<UserAnalytics/>
<DrupalWindowSync/>
{isDevMode && <Editori11y/>}

<body className="text-stone-dark">
<nav aria-label="Skip Links">
<a href="#main-content" className="skiplink">Skip to main content</a>
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@
"@graphql-codegen/typescript-graphql-request": "^6.2.0",
"@graphql-codegen/typescript-operations": "^4.2.1",
"@next/bundle-analyzer": "^14.2.3",
"@storybook/addon-essentials": "^8.1.2",
"@storybook/addon-interactions": "^8.1.2",
"@storybook/addon-links": "^8.1.2",
"@storybook/addon-essentials": "^8.1.3",
"@storybook/addon-interactions": "^8.1.3",
"@storybook/addon-links": "^8.1.3",
"@storybook/addon-styling": "^1.3.7",
"@storybook/blocks": "^8.1.2",
"@storybook/nextjs": "^8.1.2",
"@storybook/react": "^8.1.2",
"@storybook/blocks": "^8.1.3",
"@storybook/nextjs": "^8.1.3",
"@storybook/react": "^8.1.3",
"@storybook/testing-library": "^0.2.2",
"@types/react-slick": "^0.23.13",
"concurrently": "^8.2.2",
Expand All @@ -74,7 +74,7 @@
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-unused-imports": "^3.2.0",
"react-docgen": "^7.0.3",
"storybook": "^8.1.2",
"storybook": "^8.1.3",
"tsconfig-paths-webpack-plugin": "^4.1.0"
},
"packageManager": "[email protected]"
Expand Down
46 changes: 28 additions & 18 deletions src/components/elements/load-more-list.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import {useLayoutEffect, useRef, HtmlHTMLAttributes, JSX, useId} from "react";
import {useLayoutEffect, useRef, HtmlHTMLAttributes, JSX, useId, useState} from "react";
import Button from "@components/elements/button";
import {useAutoAnimate} from "@formkit/auto-animate/react";
import {useBoolean, useCounter} from "usehooks-ts";
Expand All @@ -23,19 +23,35 @@ type Props = HtmlHTMLAttributes<HTMLDivElement> & {
* The number of items per page.
*/
itemsPerPage?: number
/**
* Elements to display initially.
*/
children: JSX.Element[]
/**
* Server action callback to fetch the next "page" contents.
*/
loadPage?: (_page: number) => Promise<JSX.Element>
}

const LoadMoreList = ({buttonText, children, ulProps, liProps, itemsPerPage = 10, ...props}: Props) => {
const LoadMoreList = ({buttonText, children, ulProps, liProps, loadPage, ...props}: Props) => {
const id = useId();
const {count: shownItems, setCount: setShownItems} = useCounter(itemsPerPage)
const {count: page, increment: incrementPage} = useCounter(0)
const [items, setItems] = useState<JSX.Element[]>(children)
const {value: hasMore, setValue: setHasMore} = useBoolean(!!loadPage)
const {value: focusOnElement, setTrue: enableFocusElement, setFalse: disableFocusElement} = useBoolean(false)

const focusItemRef = useRef<HTMLLIElement>(null);
const [animationParent] = useAutoAnimate<HTMLUListElement>();

const showMoreItems = () => {
const showMoreItems = async () => {
if (loadPage) {
const results = await loadPage(page + 1);
if (results.props.children.length < 30) setHasMore(false)
setItems([...items, ...results.props.children])
}

enableFocusElement();
setShownItems(shownItems + itemsPerPage);
incrementPage()
}

const setFocusOnItem = useFocusOnRender(focusItemRef, false);
Expand All @@ -44,33 +60,27 @@ const LoadMoreList = ({buttonText, children, ulProps, liProps, itemsPerPage = 10
if (focusOnElement) setFocusOnItem()
}, [focusOnElement, setFocusOnItem]);

const focusingItem = shownItems - itemsPerPage;
const items = Array.isArray(children) ? children : [children]
const itemsToShow = items.slice(0, shownItems);
return (
<div {...props}>
<ul {...ulProps} ref={animationParent}>

{itemsToShow.map((item, i) =>
{items.map((item, i) =>
<li
key={`${id}--${i}`}
ref={focusingItem === i ? focusItemRef : null}
tabIndex={focusingItem === i && focusOnElement ? 0 : undefined}
ref={i === children.length * page ? focusItemRef : null}
tabIndex={i === children.length * page && focusOnElement ? 0 : undefined}
onBlur={disableFocusElement}
{...liProps}
>
{item}
</li>
)}
</ul>
<span className="sr-only" aria-live="polite" aria-atomic="true">
Showing {items.length} items.
</span>

{items.length > itemsPerPage &&
<span className="sr-only" aria-live="polite" aria-atomic="true">
Showing {itemsToShow.length} of {items.length} total items.
</span>
}

{items.length > shownItems &&
{hasMore &&
<Button centered onClick={showMoreItems}>
{buttonText ? buttonText : "Load More"}
</Button>
Expand Down
60 changes: 42 additions & 18 deletions src/components/elements/paged-list.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import {useLayoutEffect, useRef, HtmlHTMLAttributes, useEffect, useId} from "react";
import {useLayoutEffect, useRef, HtmlHTMLAttributes, useEffect, useId, JSX, useState} from "react";
import {useAutoAnimate} from "@formkit/auto-animate/react";
import {useBoolean, useCounter} from "usehooks-ts";
import {useRouter, useSearchParams} from "next/navigation";
Expand All @@ -16,10 +16,6 @@ type Props = HtmlHTMLAttributes<HTMLDivElement> & {
* Attributes for each <li> element.
*/
liProps?: HtmlHTMLAttributes<HTMLLIElement>,
/**
* The number of items per page.
*/
itemsPerPage?: number
/**
* URL parameter used to save the users page position.
*/
Expand All @@ -28,34 +24,47 @@ type Props = HtmlHTMLAttributes<HTMLDivElement> & {
* Number of sibling pager buttons.
*/
pagerSiblingCount?: number
/**
* Total number of pages to build the pager.
*/
totalPages: number
/**
* Server action to load a page.
*/
loadPage?: (_page: number) => Promise<JSX.Element>
}

const PagedList = ({
children,
ulProps,
liProps,
itemsPerPage = 10,
pageKey = "page",
totalPages,
pagerSiblingCount = 2,
loadPage,
...props
}: Props) => {

const id = useId();
const items = Array.isArray(children) ? children : [children]
const [items, setItems] = useState<JSX.Element[]>(Array.isArray(children) ? children : [children])
const router = useRouter();
const searchParams = useSearchParams()

// Use the GET param for page, but make sure that it is between 1 and the last page. If it's a string or a number
// outside the range, fix the value, so it works as expected.
const {
count: currentPage,
setCount: setPage
} = useCounter(Math.max(1, Math.min(Math.ceil(items.length / itemsPerPage), parseInt(searchParams.get(pageKey || "") || "") || 1)))
const {count: currentPage, setCount: setPage} = useCounter(Math.max(1, parseInt(searchParams.get(pageKey || "") || "") || 1))

const {value: focusOnElement, setTrue: enableFocusElement, setFalse: disableFocusElement} = useBoolean(false)

const focusItemRef = useRef<HTMLLIElement>(null);
const [animationParent] = useAutoAnimate<HTMLUListElement>();

const goToPage = (page: number) => {
const goToPage = async (page: number) => {
if (loadPage) {
const newView = await loadPage(page - 1)
setItems(newView.props.children)
}

enableFocusElement();
setPage(page);
}
Expand All @@ -67,7 +76,7 @@ const PagedList = ({
}, [focusOnElement, setFocusOnItem]);

useEffect(() => {
if (!pageKey) return;
if (!pageKey || !loadPage) return;

// Use search params to retain any other parameters.
const params = new URLSearchParams(searchParams.toString());
Expand All @@ -78,13 +87,28 @@ const PagedList = ({
}

router.replace(`?${params.toString()}`, {scroll: false})
}, [router, currentPage, pageKey, searchParams]);
const paginationButtons = usePagination(items.length, currentPage, itemsPerPage, pagerSiblingCount);
}, [loadPage, router, currentPage, pageKey, searchParams]);

useEffect(() => {

const updateInitialContents = async (initialPage: number) => {
if (loadPage) {
const newView = await loadPage(initialPage - 1)
setItems(newView.props.children)
}
}

const initialPage = parseInt(searchParams.get(pageKey || "") || "");
if (initialPage > 1) updateInitialContents(initialPage)
}, [searchParams, pageKey, loadPage])


const paginationButtons = usePagination(totalPages * items.length, currentPage, items.length, pagerSiblingCount);

return (
<div {...props}>
<ul {...ulProps} ref={animationParent}>
{items.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage).map((item, i) =>
{items.map((item, i) =>
<li
key={`pager-${id}-${i}`}
ref={i === 0 ? focusItemRef : null}
Expand All @@ -97,15 +121,15 @@ const PagedList = ({
)}
</ul>

{paginationButtons.length > 1 &&
{(loadPage && paginationButtons.length > 1) &&
<nav aria-label="Pager" className="mx-auto w-fit">
<ul className="list-unstyled flex gap-5">
{paginationButtons.map((pageNum, i) => (
<PaginationButton
key={`page-button-${pageNum}--${i}`}
page={pageNum}
currentPage={currentPage}
total={Math.ceil(items.length / itemsPerPage)}
total={items.length * totalPages}
onClick={() => goToPage(pageNum)}
/>
))}
Expand Down
Loading

0 comments on commit e498768

Please sign in to comment.