diff --git a/src/components/elements/load-more-list.tsx b/src/components/elements/load-more-list.tsx index 50107864..933599b0 100644 --- a/src/components/elements/load-more-list.tsx +++ b/src/components/elements/load-more-list.tsx @@ -1,10 +1,13 @@ "use client" 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" import useFocusOnRender from "@lib/hooks/useFocusOnRender" +import useServerAction from "@lib/hooks/useServerAction" +import {twMerge} from "tailwind-merge" +import {ArrowPathIcon} from "@heroicons/react/20/solid" +import Button from "@components/elements/button" type Props = HtmlHTMLAttributes & { /** @@ -31,23 +34,26 @@ type Props = HtmlHTMLAttributes & { * Server action callback to fetch the next "page" contents. */ loadPage?: (_page: number) => Promise + /** + * Count of the total number of items of all pages. + */ + totalItems: number } -const LoadMoreList = ({buttonText, children, ulProps, liProps, loadPage, ...props}: Props) => { +const LoadMoreList = ({buttonText, children, ulProps, liProps, totalItems, loadPage, ...props}: Props) => { const id = useId() const {count: page, increment: incrementPage} = useCounter(0) const [items, setItems] = useState(children) - const {value: hasMore, setValue: setHasMore} = useBoolean(!!loadPage) const {value: focusOnElement, setTrue: enableFocusElement, setFalse: disableFocusElement} = useBoolean(false) + const [runLoadPage, isPending] = useServerAction(loadPage) const focusItemRef = useRef(null) const [animationParent] = useAutoAnimate() const showMoreItems = async () => { if (loadPage) { - const results = await loadPage(page + 1) - if (results.props.children.length < 30) setHasMore(false) - setItems([...items, ...results.props.children]) + const results = await runLoadPage(page + 1) + setItems([...items, ...results?.props.children]) } enableFocusElement() @@ -61,7 +67,14 @@ const LoadMoreList = ({buttonText, children, ulProps, liProps, loadPage, ...prop }, [focusOnElement, setFocusOnItem]) return ( -
+
+ {isPending && ( +
+
+ +
+
+ )}
    {items.map((item, i) => (
  • - {hasMore && ( - )} diff --git a/src/components/paragraphs/stanford-lists/list-paragraph.tsx b/src/components/paragraphs/stanford-lists/list-paragraph.tsx index b89afd93..47c44f48 100644 --- a/src/components/paragraphs/stanford-lists/list-paragraph.tsx +++ b/src/components/paragraphs/stanford-lists/list-paragraph.tsx @@ -2,7 +2,7 @@ import Wysiwyg from "@components/elements/wysiwyg" import Button from "@components/elements/button" import View from "@components/views/view" import {H2} from "@components/elements/headers" -import {cache, ElementType, HtmlHTMLAttributes, JSX} from "react" +import {cache, ElementType, HtmlHTMLAttributes, JSX, Suspense} from "react" import { Maybe, NodeStanfordCourse, @@ -79,24 +79,26 @@ const ListParagraph = async ({paragraph, ...props}: Props) => { {viewId && displayId && viewItems && ( - + + + )} {viewItems.length === 0 && behaviors.list_paragraph?.empty_message && ( diff --git a/src/components/views/card-view-grid.tsx b/src/components/views/card-view-grid.tsx index 940034da..778b510b 100644 --- a/src/components/views/card-view-grid.tsx +++ b/src/components/views/card-view-grid.tsx @@ -11,13 +11,18 @@ type Props = { * If those nodes titles should display as

    or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items on all pages. + */ + totalItems: number } -const CardViewGrid = ({items, headingLevel}: Props) => { +const CardViewGrid = ({items, totalItems, headingLevel}: Props) => { return ( {items.map(item => ( diff --git a/src/components/views/shared-tags/shared-tags-card-view.tsx b/src/components/views/shared-tags/shared-tags-card-view.tsx index 5d04b650..4b9a743a 100644 --- a/src/components/views/shared-tags/shared-tags-card-view.tsx +++ b/src/components/views/shared-tags/shared-tags-card-view.tsx @@ -10,9 +10,13 @@ interface Props { * If those nodes titles should display as

    or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items to build the pager. + */ + totalItems: number } -const SharedTagsCardView = async ({items = [], headingLevel}: Props) => { - return +const SharedTagsCardView = async ({items = [], totalItems, headingLevel}: Props) => { + return } export default SharedTagsCardView diff --git a/src/components/views/stanford-courses/course-card-view.tsx b/src/components/views/stanford-courses/course-card-view.tsx index 61f6cc88..9083e4ac 100644 --- a/src/components/views/stanford-courses/course-card-view.tsx +++ b/src/components/views/stanford-courses/course-card-view.tsx @@ -10,9 +10,13 @@ interface Props { * If those nodes titles should display as

    or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items to build the pager. + */ + totalItems: number } -const CourseCardView = async ({items = [], headingLevel}: Props) => { - return +const CourseCardView = async ({items = [], headingLevel, totalItems}: Props) => { + return } export default CourseCardView diff --git a/src/components/views/stanford-courses/course-list-view.tsx b/src/components/views/stanford-courses/course-list-view.tsx index 3ff72eb6..800973ab 100644 --- a/src/components/views/stanford-courses/course-list-view.tsx +++ b/src/components/views/stanford-courses/course-list-view.tsx @@ -11,9 +11,13 @@ interface Props { * If those nodes titles should display as

    or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items on all pages. + */ + totalItems: number } -const CourseListView = async ({items = [], headingLevel}: Props) => { +const CourseListView = async ({items = [], totalItems, headingLevel}: Props) => { return ( { liProps={{ className: "border-b border-black-20 last-of-type:border-0 pb-10 last:pb-0 pt-10 first:pt-0", }} + totalItems={totalItems} > {items.map(item => ( diff --git a/src/components/views/stanford-events/events-card-view.tsx b/src/components/views/stanford-events/events-card-view.tsx index 6583535c..22714bc4 100644 --- a/src/components/views/stanford-events/events-card-view.tsx +++ b/src/components/views/stanford-events/events-card-view.tsx @@ -10,9 +10,13 @@ interface Props { * If those nodes titles should display as

    or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items to build the pager. + */ + totalItems: number } -const EventsCardView = async ({items = [], headingLevel}: Props) => { - return +const EventsCardView = async ({items = [], headingLevel, totalItems}: Props) => { + return } export default EventsCardView diff --git a/src/components/views/stanford-events/events-filtered-list-view.tsx b/src/components/views/stanford-events/events-filtered-list-view.tsx index 92b0b44d..5cb425e3 100644 --- a/src/components/views/stanford-events/events-filtered-list-view.tsx +++ b/src/components/views/stanford-events/events-filtered-list-view.tsx @@ -30,7 +30,15 @@ const getTopicOptions = ( return topicOptions.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)) } -const EventsFilteredListView = ({items, topics}: {items: NodeStanfordEvent[]; topics: TermStanfordEventType[]}) => { +const EventsFilteredListView = ({ + items, + totalItems, + topics, +}: { + items: NodeStanfordEvent[] + totalItems: number + topics: TermStanfordEventType[] +}) => { const [chosenTopic, setChosenTopic] = useState("") const [displayedEvents, setDisplayedEvents] = useState(items) @@ -88,6 +96,7 @@ const EventsFilteredListView = ({items, topics}: {items: NodeStanfordEvent[]; to className: "border-b border-black-20 last-of-type:border-0 pb-10 last:pb-0 pt-10 first:pt-0", }} itemsPerPage={3} + totalItems={totalItems} > {displayedEvents.map(event => ( diff --git a/src/components/views/stanford-events/events-list-view.tsx b/src/components/views/stanford-events/events-list-view.tsx index 0eb59465..7b1799b6 100644 --- a/src/components/views/stanford-events/events-list-view.tsx +++ b/src/components/views/stanford-events/events-list-view.tsx @@ -12,16 +12,20 @@ interface Props { * If those nodes titles should display as

    or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items on all pages. + */ + totalItems: number } -const EventsListView = async ({items = [], headingLevel}: Props) => { +const EventsListView = async ({items = [], headingLevel, totalItems}: Props) => { if (items.length >= 5) { const topics: TermStanfordEventType[] = [] items.map(event => event.suEventType?.map(topic => topics.push(topic))) const uniqueTopics = [...new Map(topics.map(t => [t.id, t])).values()] if (uniqueTopics.length > 1) { - return + return } } @@ -36,6 +40,7 @@ const EventsListView = async ({items = [], headingLevel}: Props) => { liProps={{ className: "border-b border-black-20 last-of-type:border-0 pb-10 last:pb-0 pt-10 first:pt-0", }} + totalItems={totalItems} > {items.map(item => ( diff --git a/src/components/views/stanford-news/news-card-view.tsx b/src/components/views/stanford-news/news-card-view.tsx index 8ec3fb2e..38b8e9ab 100644 --- a/src/components/views/stanford-news/news-card-view.tsx +++ b/src/components/views/stanford-news/news-card-view.tsx @@ -10,9 +10,13 @@ interface Props { * If those nodes titles should display as

    or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items to build the pager. + */ + totalItems: number } -const NewsCardView = async ({items = [], headingLevel}: Props) => { - return +const NewsCardView = async ({items = [], headingLevel, totalItems}: Props) => { + return } export default NewsCardView diff --git a/src/components/views/stanford-news/news-list-view.tsx b/src/components/views/stanford-news/news-list-view.tsx index dfa5a667..c7f50a18 100644 --- a/src/components/views/stanford-news/news-list-view.tsx +++ b/src/components/views/stanford-news/news-list-view.tsx @@ -22,7 +22,7 @@ interface Props { loadPage?: (_page: number) => Promise } -const NewsListView = async ({items, totalItems, headingLevel, loadPage}: Props) => { +const NewsListView = async ({items, headingLevel, totalItems, loadPage}: Props) => { return ( or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items to build the pager. + */ + totalItems: number } -const PageCardView = async ({items = [], headingLevel}: Props) => { - return +const PageCardView = async ({items = [], headingLevel, totalItems}: Props) => { + return } export default PageCardView diff --git a/src/components/views/stanford-page/page-list-view.tsx b/src/components/views/stanford-page/page-list-view.tsx index 16fef399..b25fe626 100644 --- a/src/components/views/stanford-page/page-list-view.tsx +++ b/src/components/views/stanford-page/page-list-view.tsx @@ -11,15 +11,20 @@ interface Props { * If those nodes titles should display as

    or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items on all pages. + */ + totalItems: number } -const PageListView = async ({items = [], headingLevel}: Props) => { +const PageListView = async ({items = [], headingLevel, totalItems}: Props) => { return ( {items.map(item => ( diff --git a/src/components/views/stanford-person/person-card-view.tsx b/src/components/views/stanford-person/person-card-view.tsx index c2bd14dd..7d667729 100644 --- a/src/components/views/stanford-person/person-card-view.tsx +++ b/src/components/views/stanford-person/person-card-view.tsx @@ -10,9 +10,13 @@ interface Props { * If those nodes titles should display as

    or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items to build the pager. + */ + totalItems: number } -const PersonCardView = async ({items = [], headingLevel}: Props) => { - return +const PersonCardView = async ({items = [], headingLevel, totalItems}: Props) => { + return } export default PersonCardView diff --git a/src/components/views/stanford-publications/publications-apa-view.tsx b/src/components/views/stanford-publications/publications-apa-view.tsx index ebd3ebde..9ed9ca38 100644 --- a/src/components/views/stanford-publications/publications-apa-view.tsx +++ b/src/components/views/stanford-publications/publications-apa-view.tsx @@ -10,9 +10,13 @@ interface Props { * If those nodes titles should display as

    or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items to build the pager. + */ + totalItems: number } -const PublicationsApaView = async ({items = [], headingLevel}: Props) => { - return +const PublicationsApaView = async ({items = [], headingLevel, totalItems}: Props) => { + return } export default PublicationsApaView diff --git a/src/components/views/stanford-publications/publications-chicago-view.tsx b/src/components/views/stanford-publications/publications-chicago-view.tsx index 7d2f051d..18b23c81 100644 --- a/src/components/views/stanford-publications/publications-chicago-view.tsx +++ b/src/components/views/stanford-publications/publications-chicago-view.tsx @@ -11,15 +11,20 @@ interface Props { * If those nodes titles should display as

    or

    */ headingLevel?: "h2" | "h3" + /** + * Total number of items on all pages. + */ + totalItems: number } -const PublicationsChicagoView = async ({items = [], headingLevel}: Props) => { +const PublicationsChicagoView = async ({items = [], headingLevel, totalItems}: Props) => { return ( {items.map(item => ( diff --git a/src/components/views/view.tsx b/src/components/views/view.tsx index 52f17f53..cca2075a 100644 --- a/src/components/views/view.tsx +++ b/src/components/views/view.tsx @@ -1,8 +1,8 @@ -import {JSX, Suspense} from "react" +import {JSX} from "react" import SharedTagsCardView from "@components/views/shared-tags/shared-tags-card-view" import PageListView from "@components/views/stanford-page/page-list-view" import NewsCardView from "@components/views/stanford-news/news-card-view" -import NewsListView, {NewsListSkeleton} from "@components/views/stanford-news/news-list-view" +import NewsListView from "@components/views/stanford-news/news-list-view" import PersonCardView from "@components/views/stanford-person/person-card-view" import EventsCardView from "@components/views/stanford-events/events-card-view" import EventsListView from "@components/views/stanford-events/events-list-view" @@ -53,51 +53,67 @@ const View = async ({viewId, displayId, items, totalItems, loadPage, headingLeve switch (component) { case "stanford_basic_pages--basic_page_type_list": - return + return case "stanford_news--vertical_cards": - return + return case "stanford_news--block_1": return ( - }> - - + ) case "stanford_person--grid_list_all": - return + return ( + + ) case "stanford_events--cards": - return + return case "stanford_events--past_events_list_block": case "stanford_events--list_page": - return + return case "stanford_basic_pages--viewfield_block_1": case "stanford_basic_pages--card_grid_alpha": - return + return case "stanford_shared_tags--card_grid": - return + return case "stanford_courses--default_list_viewfield_block": - return + return ( + + ) case "stanford_courses--vertical_teaser_viewfield_block": - return + return ( + + ) case "stanford_publications--apa_list": - return + return ( + + ) case "stanford_publications--chicago_list": - return + return ( + + ) } } export default View diff --git a/src/lib/hooks/useFocusOnRender.tsx b/src/lib/hooks/useFocusOnRender.tsx index 169e1881..b57991b4 100644 --- a/src/lib/hooks/useFocusOnRender.tsx +++ b/src/lib/hooks/useFocusOnRender.tsx @@ -20,7 +20,11 @@ const useFocusOnRender = (focusOnElement: RefObject, defaultFocus: useLayoutEffect(() => { if (value) { const reduceMotion = !!window.matchMedia("(prefers-reduced-motion: reduce)")?.matches - focusOnElement.current?.scrollIntoView({behavior: reduceMotion ? "instant" : "smooth"}) + focusOnElement.current?.scrollIntoView({ + behavior: reduceMotion ? "instant" : "smooth", + block: "nearest", + inline: "start", + }) focusOnElement.current?.focus({preventScroll: true}) setFalse()