Move root to swr
This commit is contained in:
parent
b0d21e85cc
commit
d20d1b5f73
@ -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
14
pnpm-lock.yaml
generated
@ -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)):
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
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,
|
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 });
|
||||||
|
|||||||
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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user