Move root to swr

This commit is contained in:
Sam Becker 2024-04-24 19:48:48 -05:00
parent b0d21e85cc
commit d20d1b5f73
8 changed files with 168 additions and 35 deletions

View File

@ -51,6 +51,7 @@
"react-dom": "18.2.0",
"react-icons": "^5.1.0",
"sonner": "^1.4.41",
"swr": "^2.2.5",
"tailwindcss": "3.4.3",
"ts-exif-parser": "^0.2.2",
"typescript": "5.4.5",

14
pnpm-lock.yaml generated
View File

@ -134,6 +134,9 @@ importers:
sonner:
specifier: ^1.4.41
version: 1.4.41(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
swr:
specifier: ^2.2.5
version: 2.2.5(react@18.2.0)
tailwindcss:
specifier: 3.4.3
version: 3.4.3
@ -3770,6 +3773,11 @@ packages:
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0
swr@2.2.5:
resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0
swrev@4.0.0:
resolution: {integrity: sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==}
@ -8733,6 +8741,12 @@ snapshots:
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
swr@2.2.5(react@18.2.0):
dependencies:
client-only: 0.0.1
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
swrev@4.0.0: {}
swrv@1.0.4(vue@3.4.21(typescript@5.4.5)):

View File

@ -12,6 +12,7 @@ import { ThemeProvider } from 'next-themes';
import Nav from '@/site/Nav';
import Footer from '@/site/Footer';
import CommandK from '@/site/CommandK';
import SWRConfigClient from '../state/SWRConfigClient';
import '../site/globals.css';
import '../site/sonner.css';
@ -72,24 +73,26 @@ export default function RootLayout({
>
<body className={ibmPlexMono.variable}>
<AppStateProvider>
<MoreComponentsProvider>
<ThemeProvider attribute="class">
<main className={clsx(
'mx-3 mb-3',
'lg:mx-6 lg:mb-6',
)}>
<Nav />
<div className={clsx(
'min-h-[16rem] sm:min-h-[30rem]',
'mb-12',
<SWRConfigClient>
<MoreComponentsProvider>
<ThemeProvider attribute="class">
<main className={clsx(
'mx-3 mb-3',
'lg:mx-6 lg:mb-6',
)}>
{children}
</div>
<Footer />
</main>
<CommandK />
</ThemeProvider>
</MoreComponentsProvider>
<Nav />
<div className={clsx(
'min-h-[16rem] sm:min-h-[30rem]',
'mb-12',
)}>
{children}
</div>
<Footer />
</main>
<CommandK />
</ThemeProvider>
</MoreComponentsProvider>
</SWRConfigClient>
<Analytics debug={false} />
<SpeedInsights debug={false} />
<PhotoEscapeHandler />

View File

@ -1,4 +1,4 @@
import { getPhotosCached, getPhotosCountCached } from '@/photo/cache';
import { getPhotosCachedCached } from '@/photo/cache';
import {
INFINITE_SCROLL_MULTIPLE_HOME,
generateOgImageMetaForPhotos,
@ -7,26 +7,25 @@ import PhotosEmptyState from '@/photo/PhotosEmptyState';
import { Metadata } from 'next/types';
import { MAX_PHOTOS_TO_SHOW_OG } from '@/image-response';
import PhotosLarge from '@/photo/PhotosLarge';
import { MorePhotosRoot } from '@/photo/MorePhotosRoot';
import InfinitePhotoScroll from '../photo/InfinitePhotoScroll';
export const dynamic = 'force-static';
export async function generateMetadata(): Promise<Metadata> {
// Make homepage queries resilient to error on first time setup
const photos = await getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_OG })
const photos = await getPhotosCachedCached({
limit: MAX_PHOTOS_TO_SHOW_OG,
})
// Make homepage queries resilient to error on first time setup
.catch(() => []);
return generateOgImageMetaForPhotos(photos);
}
export default async function HomePage() {
const [
photos,
count,
] = await Promise.all([
const photos = await getPhotosCachedCached({
limit: INFINITE_SCROLL_MULTIPLE_HOME,
})
// Make homepage queries resilient to error on first time setup
getPhotosCached({ limit: INFINITE_SCROLL_MULTIPLE_HOME }).catch(() => []),
getPhotosCountCached().catch(() => 0),
]);
.catch(() => []);
return (
photos.length > 0
@ -35,10 +34,9 @@ export default async function HomePage() {
photos={photos}
prefetchFirstPhotoLinks={true}
/>
<MorePhotosRoot
<InfinitePhotoScroll
initialOffset={INFINITE_SCROLL_MULTIPLE_HOME}
itemsPerRequest={INFINITE_SCROLL_MULTIPLE_HOME}
totalPhotosCount={count}
itemsPerPage={INFINITE_SCROLL_MULTIPLE_HOME}
/>
</div>
: <PhotosEmptyState />

View File

@ -0,0 +1,99 @@
'use client';
import { preload } from 'swr';
import useSwrInfinite from 'swr/infinite';
import PhotosLarge from '@/photo/PhotosLarge';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import SiteGrid from '@/components/SiteGrid';
import Spinner from '@/components/Spinner';
import { getPhotosAction } from '@/photo/actions';
export default function InfinitePhotoScroll({
key = 'PHOTOS',
initialOffset = 0,
itemsPerPage = 12,
prefetch = true,
triggerOnView = true,
debug,
}: {
key?: string
initialOffset?: number
itemsPerPage?: number
prefetch?: boolean
triggerOnView?: boolean
debug?: boolean
}) {
const buttonRef = useRef<HTMLButtonElement>(null);
const fetcher = useCallback((key: string) => {
const offset = parseInt(key.split('-')[1]);
if (debug) { console.log('Fetching', offset); }
return getPhotosAction(
initialOffset + offset * itemsPerPage,
itemsPerPage,
);
}, [initialOffset, itemsPerPage, debug]);
const { data, isLoading, error, mutate, size, setSize } = useSwrInfinite(
(size, prev: []) => prev && prev.length === 0
? null
:`${key}-${size}`,
fetcher,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateFirstPage: false,
}
);
const isFinished = useMemo(() =>
data && data[data.length - 1]?.length < itemsPerPage
, [data, itemsPerPage]);
useEffect(() => {
if (prefetch) {
preload(`${key}-${size}`, fetcher);
}
}, [prefetch, key, size, fetcher]);
const advance = useCallback(() => setSize(size => size + 1), [setSize]);
useEffect(() => {
// Only add observer if button is rendered
if (buttonRef.current) {
const observer = new IntersectionObserver(e => {
if (triggerOnView && e[0].isIntersecting) {
advance();
}
}, {
root: null,
threshold: 0,
});
observer.observe(buttonRef.current);
return () => observer.disconnect();
}
}, [triggerOnView, advance]);
const photos = useMemo(() => data?.flat(), [data]);
return (
<div className="space-y-4">
{photos && <PhotosLarge photos={photos} />}
{!isFinished &&
<SiteGrid contentMain={
<button
ref={buttonRef}
onClick={error ? () => mutate() : advance}
disabled={isLoading}
className="w-full flex justify-center"
>
{error
? 'Try Again'
: isLoading
? <Spinner size={20} />
: 'Load More'}
</button>
} />}
</div>
);
}

View File

@ -20,6 +20,7 @@ import {
deleteStorageUrl,
} from '@/services/storage';
import {
getPhotosCachedCached,
revalidateAdminPaths,
revalidateAllKeysAndPaths,
revalidatePhoto,
@ -218,3 +219,6 @@ export async function getPhotoItemsAction(query: string) {
}]
: [];
}
export const getPhotosAction = async (offset: number, limit: number) =>
getPhotosCachedCached({ offset, limit });

View File

@ -0,0 +1,13 @@
'use client';
import { SWRConfig } from 'swr';
export default function SWRConfigClient({
children,
}: {
children: React.ReactNode
}) {
return <SWRConfig>
{children}
</SWRConfig>;
}

View File

@ -2,6 +2,7 @@ import PhotoTag from '@/tag/PhotoTag';
import { isTagFavs } from '.';
import FavsTag from './FavsTag';
import { EntityLinkExternalProps } from '@/components/primitives/EntityLink';
import { Fragment } from 'react';
export default function PhotoTags({
tags,
@ -13,11 +14,11 @@ export default function PhotoTags({
return (
<div className="flex flex-col">
{tags.map(tag =>
<>
<Fragment key={tag}>
{isTagFavs(tag)
? <FavsTag key={tag} {...{ contrast, prefetch }} />
: <PhotoTag key={tag} {...{ tag, contrast, prefetch }} />}
</>)}
? <FavsTag {...{ contrast, prefetch }} />
: <PhotoTag {...{ tag, contrast, prefetch }} />}
</Fragment>)}
</div>
);
}