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 eb60576 commit 92502e2
Show file tree
Hide file tree
Showing 8 changed files with 1,999 additions and 919 deletions.
38 changes: 19 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
"@formkit/auto-animate": "^0.8.2",
"@heroicons/react": "^2.1.3",
"@js-temporal/polyfill": "^0.4.4",
"@mui/base": "^5.0.0-beta.42",
"@mui/base": "^5.0.0-beta.46",
"@next/third-parties": "^14.2.3",
"@tailwindcss/container-queries": "^0.1.1",
"@types/node": "^20.12.7",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@types/node": "^20.12.12",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"algoliasearch": "^4.23.3",
"autoprefixer": "^10.4.19",
"axios": "^1.6.8",
"axios": "^1.7.2",
"clsx": "^2.1.1",
"decanter": "^7.3.0",
"drupal-jsonapi-params": "^2.3.1",
Expand All @@ -39,14 +39,14 @@
"next-drupal": "^1.6.0",
"postcss": "^8.4.38",
"qs": "^6.12.1",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-focus-lock": "^2.12.1",
"react-instantsearch": "^7.7.2",
"react-instantsearch-nextjs": "^0.2.1",
"react-instantsearch": "^7.9.0",
"react-instantsearch-nextjs": "^0.2.5",
"react-slick": "^0.30.2",
"react-tiny-oembed": "^1.1.0",
"sharp": "^0.33.3",
"sharp": "^0.33.4",
"tailwind-merge": "^2.3.0",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
Expand All @@ -57,24 +57,24 @@
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/import-types-preset": "^3.0.0",
"@graphql-codegen/typescript-graphql-request": "^6.2.0",
"@graphql-codegen/typescript-operations": "^4.2.0",
"@graphql-codegen/typescript-operations": "^4.2.1",
"@next/bundle-analyzer": "^14.2.3",
"@storybook/addon-essentials": "^8.0.9",
"@storybook/addon-interactions": "^8.0.9",
"@storybook/addon-links": "^8.0.9",
"@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.0.9",
"@storybook/nextjs": "^8.0.9",
"@storybook/react": "^8.0.9",
"@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",
"encoding": "^0.1.13",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-unused-imports": "^3.1.0",
"eslint-plugin-unused-imports": "^3.2.0",
"react-docgen": "^7.0.3",
"storybook": "^8.0.9",
"storybook": "^8.1.3",
"tsconfig-paths-webpack-plugin": "^4.1.0"
},
"packageManager": "yarn@4.1.1"
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
98 changes: 69 additions & 29 deletions src/components/elements/paged-list.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import {useLayoutEffect, useRef, HtmlHTMLAttributes, useEffect} from "react";
import Button from "@components/elements/button";
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 @@ -18,30 +17,54 @@ type Props = HtmlHTMLAttributes<HTMLDivElement> & {
*/
liProps?: HtmlHTMLAttributes<HTMLLIElement>,
/**
* The number of items per page.
* URL parameter used to save the users page position.
*/
itemsPerPage?: number
pageKey?: string | false
/**
* URL parameter used to save the users page position.
* Number of sibling pager buttons.
*/
pagerSiblingCount?: number
/**
* Total number of pages to build the pager.
*/
totalPages: number
/**
* Server action to load a page.
*/
pageKey?: string
loadPage?: (_page: number) => Promise<JSX.Element>
}

const PagedList = ({children, ulProps, liProps, itemsPerPage = 10, pageKey = "page", ...props}: Props) => {
const items = Array.isArray(children) ? children : [children]

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

const id = useId();
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
// 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: page, 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 @@ -53,24 +76,41 @@ const PagedList = ({children, ulProps, liProps, itemsPerPage = 10, pageKey = "pa
}, [focusOnElement, setFocusOnItem]);

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

// Use search params to retain any other parameters.
const params = new URLSearchParams(searchParams.toString());
if (page > 1) {
params.set(pageKey, `${page}`)
if (currentPage > 1) {
params.set(pageKey, `${currentPage}`)
} else {
params.delete(pageKey)
}

router.replace(`?${params.toString()}`, {scroll: false})
}, [router, page, pageKey, searchParams]);
const paginationButtons = usePagination(items.length, page, itemsPerPage, 2);
}, [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((page - 1) * itemsPerPage, page * itemsPerPage).map((item, i) =>
{items.map((item, i) =>
<li
key={`pager--${i}`}
key={`pager-${id}-${i}`}
ref={i === 0 ? focusItemRef : null}
tabIndex={i === 0 && focusOnElement ? 0 : undefined}
onBlur={disableFocusElement}
Expand All @@ -81,15 +121,15 @@ const PagedList = ({children, ulProps, liProps, itemsPerPage = 10, pageKey = "pa
)}
</ul>

{paginationButtons.length > 1 &&
<nav aria-label="Pager">
<ul className="list-unstyled flex justify-between">
{(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={page}
total={Math.ceil(items.length / itemsPerPage)}
currentPage={currentPage}
total={items.length * totalPages}
onClick={() => goToPage(pageNum)}
/>
))}
Expand All @@ -114,17 +154,17 @@ const PaginationButton = ({page, currentPage, total, onClick}: {
</li>
)
}

const isCurrent = page == currentPage;
return (
<li>
<Button
className={page === currentPage ? "bg-black text-white" : ""}
<button
className="font-medium hocus:underline text-m2"
onClick={onClick}
aria-current={page == currentPage || undefined}
aria-current={isCurrent}
>
<span className="sr-only">Go to page {page} of {total}</span>
<span aria-hidden>{page}</span>
</Button>
<span aria-hidden className={(isCurrent ? "text-digital-red border-digital-red" : "text-cardinal-red border-transparent") + " border-b-2 px-4"}>{page}</span>
</button>
</li>
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/elements/user-analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {isPreviewMode} from "@lib/drupal/utils";
const UserAnalytics = async () => {
if (isPreviewMode()) return;
const siteSettingsConfig = await getConfigPage<StanfordBasicSiteSetting>("StanfordBasicSiteSetting")
if (!siteSettingsConfig?.suGoogleAnalytics) return;
if (!siteSettingsConfig?.suGoogleAnalytics || !process.env.NEXT_PUBLIC_DOMAIN) return;
return (
<>
<Script async src="//siteimproveanalytics.com/js/siteanalyze_80352.js"/>
Expand Down
Loading

0 comments on commit 92502e2

Please sign in to comment.