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-dom": "18.2.0",
"react-icons": "^5.1.0", "react-icons": "^5.1.0",
"sonner": "^1.4.41", "sonner": "^1.4.41",
"swr": "^2.2.5",
"tailwindcss": "3.4.3", "tailwindcss": "3.4.3",
"ts-exif-parser": "^0.2.2", "ts-exif-parser": "^0.2.2",
"typescript": "5.4.5", "typescript": "5.4.5",

14
pnpm-lock.yaml generated
View File

@ -134,6 +134,9 @@ importers:
sonner: sonner:
specifier: ^1.4.41 specifier: ^1.4.41
version: 1.4.41(react-dom@18.2.0(react@18.2.0))(react@18.2.0) 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: tailwindcss:
specifier: 3.4.3 specifier: 3.4.3
version: 3.4.3 version: 3.4.3
@ -3770,6 +3773,11 @@ packages:
peerDependencies: peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 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: swrev@4.0.0:
resolution: {integrity: sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==} resolution: {integrity: sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==}
@ -8733,6 +8741,12 @@ snapshots:
react: 18.2.0 react: 18.2.0
use-sync-external-store: 1.2.0(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: {} swrev@4.0.0: {}
swrv@1.0.4(vue@3.4.21(typescript@5.4.5)): 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 Nav from '@/site/Nav';
import Footer from '@/site/Footer'; import Footer from '@/site/Footer';
import CommandK from '@/site/CommandK'; import CommandK from '@/site/CommandK';
import SWRConfigClient from '../state/SWRConfigClient';
import '../site/globals.css'; import '../site/globals.css';
import '../site/sonner.css'; import '../site/sonner.css';
@ -72,24 +73,26 @@ export default function RootLayout({
> >
<body className={ibmPlexMono.variable}> <body className={ibmPlexMono.variable}>
<AppStateProvider> <AppStateProvider>
<MoreComponentsProvider> <SWRConfigClient>
<ThemeProvider attribute="class"> <MoreComponentsProvider>
<main className={clsx( <ThemeProvider attribute="class">
'mx-3 mb-3', <main className={clsx(
'lg:mx-6 lg:mb-6', 'mx-3 mb-3',
)}> 'lg:mx-6 lg:mb-6',
<Nav />
<div className={clsx(
'min-h-[16rem] sm:min-h-[30rem]',
'mb-12',
)}> )}>
{children} <Nav />
</div> <div className={clsx(
<Footer /> 'min-h-[16rem] sm:min-h-[30rem]',
</main> 'mb-12',
<CommandK /> )}>
</ThemeProvider> {children}
</MoreComponentsProvider> </div>
<Footer />
</main>
<CommandK />
</ThemeProvider>
</MoreComponentsProvider>
</SWRConfigClient>
<Analytics debug={false} /> <Analytics debug={false} />
<SpeedInsights debug={false} /> <SpeedInsights debug={false} />
<PhotoEscapeHandler /> <PhotoEscapeHandler />

View File

@ -1,4 +1,4 @@
import { getPhotosCached, getPhotosCountCached } from '@/photo/cache'; import { getPhotosCachedCached } from '@/photo/cache';
import { import {
INFINITE_SCROLL_MULTIPLE_HOME, INFINITE_SCROLL_MULTIPLE_HOME,
generateOgImageMetaForPhotos, generateOgImageMetaForPhotos,
@ -7,26 +7,25 @@ import PhotosEmptyState from '@/photo/PhotosEmptyState';
import { Metadata } from 'next/types'; import { Metadata } from 'next/types';
import { MAX_PHOTOS_TO_SHOW_OG } from '@/image-response'; import { MAX_PHOTOS_TO_SHOW_OG } from '@/image-response';
import PhotosLarge from '@/photo/PhotosLarge'; import PhotosLarge from '@/photo/PhotosLarge';
import { MorePhotosRoot } from '@/photo/MorePhotosRoot'; import InfinitePhotoScroll from '../photo/InfinitePhotoScroll';
export const dynamic = 'force-static'; export const dynamic = 'force-static';
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
// Make homepage queries resilient to error on first time setup const photos = await getPhotosCachedCached({
const photos = await getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_OG }) limit: MAX_PHOTOS_TO_SHOW_OG,
})
// Make homepage queries resilient to error on first time setup
.catch(() => []); .catch(() => []);
return generateOgImageMetaForPhotos(photos); return generateOgImageMetaForPhotos(photos);
} }
export default async function HomePage() { export default async function HomePage() {
const [ const photos = await getPhotosCachedCached({
photos, limit: INFINITE_SCROLL_MULTIPLE_HOME,
count, })
] = await Promise.all([
// Make homepage queries resilient to error on first time setup // Make homepage queries resilient to error on first time setup
getPhotosCached({ limit: INFINITE_SCROLL_MULTIPLE_HOME }).catch(() => []), .catch(() => []);
getPhotosCountCached().catch(() => 0),
]);
return ( return (
photos.length > 0 photos.length > 0
@ -35,10 +34,9 @@ export default async function HomePage() {
photos={photos} photos={photos}
prefetchFirstPhotoLinks={true} prefetchFirstPhotoLinks={true}
/> />
<MorePhotosRoot <InfinitePhotoScroll
initialOffset={INFINITE_SCROLL_MULTIPLE_HOME} initialOffset={INFINITE_SCROLL_MULTIPLE_HOME}
itemsPerRequest={INFINITE_SCROLL_MULTIPLE_HOME} itemsPerPage={INFINITE_SCROLL_MULTIPLE_HOME}
totalPhotosCount={count}
/> />
</div> </div>
: <PhotosEmptyState /> : <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, deleteStorageUrl,
} from '@/services/storage'; } from '@/services/storage';
import { import {
getPhotosCachedCached,
revalidateAdminPaths, revalidateAdminPaths,
revalidateAllKeysAndPaths, revalidateAllKeysAndPaths,
revalidatePhoto, 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 { isTagFavs } from '.';
import FavsTag from './FavsTag'; import FavsTag from './FavsTag';
import { EntityLinkExternalProps } from '@/components/primitives/EntityLink'; import { EntityLinkExternalProps } from '@/components/primitives/EntityLink';
import { Fragment } from 'react';
export default function PhotoTags({ export default function PhotoTags({
tags, tags,
@ -13,11 +14,11 @@ export default function PhotoTags({
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
{tags.map(tag => {tags.map(tag =>
<> <Fragment key={tag}>
{isTagFavs(tag) {isTagFavs(tag)
? <FavsTag key={tag} {...{ contrast, prefetch }} /> ? <FavsTag {...{ contrast, prefetch }} />
: <PhotoTag key={tag} {...{ tag, contrast, prefetch }} />} : <PhotoTag {...{ tag, contrast, prefetch }} />}
</>)} </Fragment>)}
</div> </div>
); );
} }