Move root to swr
This commit is contained in:
parent
b0d21e85cc
commit
d20d1b5f73
@ -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
14
pnpm-lock.yaml
generated
@ -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)):
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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 />
|
||||
|
||||
99
src/photo/InfinitePhotoScroll.tsx
Normal file
99
src/photo/InfinitePhotoScroll.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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 });
|
||||
|
||||
13
src/state/SWRConfigClient.tsx
Normal file
13
src/state/SWRConfigClient.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
export default function SWRConfigClient({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <SWRConfig>
|
||||
{children}
|
||||
</SWRConfig>;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user