Remove legacy infinite scroll system

This commit is contained in:
Sam Becker 2024-04-27 12:55:51 -05:00
parent 8ef0283822
commit bd0f61f237
8 changed files with 15 additions and 496 deletions

View File

@ -2,19 +2,13 @@
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import { syncCacheAction } from '@/photo/actions';
import { useMoreComponentsState } from '@/state/MoreComponentsState';
import { BiTrash } from 'react-icons/bi';
export default function ClearCacheButton() {
const {
clearMoreComponentsState,
} = useMoreComponentsState();
return (
<form action={syncCacheAction}>
<SubmitButtonWithStatus
icon={<BiTrash />}
onFormSubmit={clearMoreComponentsState}
>
Clear Cache
</SubmitButtonWithStatus>

View File

@ -7,7 +7,6 @@ import AppStateProvider from '@/state/AppStateProvider';
import ToasterWithThemes from '@/toast/ToasterWithThemes';
import PhotoEscapeHandler from '@/photo/PhotoEscapeHandler';
import { Metadata } from 'next/types';
import MoreComponentsProvider from '@/state/MoreComponentsProvider';
import { ThemeProvider } from 'next-themes';
import Nav from '@/site/Nav';
import Footer from '@/site/Footer';
@ -74,24 +73,22 @@ export default function RootLayout({
<body className={ibmPlexMono.variable}>
<AppStateProvider>
<SwrConfigClient>
<MoreComponentsProvider>
<ThemeProvider attribute="class">
<main className={clsx(
'mx-3 mb-3',
'lg:mx-6 lg:mb-6',
<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',
)}>
<Nav />
<div className={clsx(
'min-h-[16rem] sm:min-h-[30rem]',
'mb-12',
)}>
{children}
</div>
<Footer />
</main>
<CommandK />
</ThemeProvider>
</MoreComponentsProvider>
{children}
</div>
<Footer />
</main>
<CommandK />
</ThemeProvider>
</SwrConfigClient>
<Analytics debug={false} />
<SpeedInsights debug={false} />

View File

@ -1,252 +0,0 @@
'use client';
import { useCallback, useEffect, useRef } from 'react';
import Spinner from './Spinner';
import SiteGrid from './SiteGrid';
import {
MoreComponentsKey,
MoreComponentsStateForKeyArgument,
useMoreComponentsState,
} from '@/state/MoreComponentsState';
const MAX_ATTEMPTS_PER_REQUEST = 5;
const MAX_TOTAL_REQUESTS = 100;
const RETRY_DELAY_IN_SECONDS = 1;
export default function MoreComponents({
stateKey,
initialOffset,
itemsPerRequest,
getNextComponent,
label = 'Load more',
triggerOnView = true,
prefetch = true,
itemsClass,
wrapMoreButtonInSiteGrid,
debug,
}: {
stateKey: MoreComponentsKey
initialOffset: number
itemsPerRequest: number
getNextComponent: (offset: number, limit: number) => Promise<{
nextComponent?: JSX.Element,
isFinished?: boolean,
didFail?: boolean,
}>
label?: string
triggerOnView?: boolean
prefetch?: boolean
itemsClass?: string
wrapMoreButtonInSiteGrid?: boolean
debug?: boolean
}) {
const { state, setStateForKey } = useMoreComponentsState();
const setState = useCallback(
(stateForKey: MoreComponentsStateForKeyArgument) =>
setStateForKey(stateKey, stateForKey),
[setStateForKey, stateKey]);
useEffect(() => {
setState({ hasMounted: true });
}, [setState]);
const {
hasMounted,
isLoading,
indexInView,
finalIndex,
didReachMaximumRequests,
components,
} = state[stateKey];
// When prefetching, always stay one request ahead of what's visible
const furthestIndexToLoad = Math.min(
prefetch ? (indexInView ?? 0) + 1 : (indexInView ?? 0),
finalIndex ?? Infinity,
);
const indexToLoad = Math.min(
components.length,
furthestIndexToLoad,
);
const attemptsPerRequest = useRef(0);
const totalRequests = useRef(0);
const hasFinalIndexBeenReached =
finalIndex !== undefined &&
finalIndex <= components.length - 1;
const areAllComponentsVisible =
(indexInView ?? 0) >= components.length - 1;
const showMoreButton =
isLoading ||
!hasFinalIndexBeenReached ||
!areAllComponentsVisible;
const currentTimeout = useRef<NodeJS.Timeout>();
const attempt = useCallback(() => {
// Consider creating temp, anonymous function
// for error handling
const attemptRetry = () => {
if (currentTimeout.current) {
clearTimeout(currentTimeout.current);
}
currentTimeout.current =
setTimeout(attempt, RETRY_DELAY_IN_SECONDS * 1000);
};
if (attemptsPerRequest.current < MAX_ATTEMPTS_PER_REQUEST) {
if (totalRequests.current < MAX_TOTAL_REQUESTS) {
attemptsPerRequest.current += 1;
totalRequests.current += 1;
setState({ isLoading: true });
if (debug) {
// eslint-disable-next-line max-len
console.log(`GETTING INDEX: #${indexToLoad}, ATTEMPT: #${attemptsPerRequest.current}`);
}
getNextComponent(
initialOffset + indexToLoad * itemsPerRequest,
itemsPerRequest,
)
.then(({ nextComponent, isFinished, didFail }) => {
if (!didFail) {
attemptsPerRequest.current = 0;
setState(state => {
const updatedComponents = [...state.components];
if (nextComponent) {
updatedComponents[indexToLoad] = nextComponent;
}
return {
...state,
...nextComponent && { components: updatedComponents},
latestIndexLoaded: indexToLoad,
isLoading: false,
didReachMaximumRequests: false,
...isFinished && {
// Special case when finished on first request
finalIndex: indexToLoad === 0 ? -1 : indexToLoad,
},
};
});
} else {
attemptRetry();
}
})
.catch(attemptRetry);
} else {
console.log(
`Max total attempts reached (${MAX_TOTAL_REQUESTS})`
);
setState({
isLoading: false,
didReachMaximumRequests: true,
});
}
} else {
console.log(
`Max attempts per request reached ${MAX_ATTEMPTS_PER_REQUEST}`
);
setState({
isLoading: false,
didReachMaximumRequests: true,
});
}
}, [
setState,
getNextComponent,
initialOffset,
indexToLoad,
itemsPerRequest,
debug,
]);
useEffect(() => {
if (
!isLoading &&
indexToLoad >= components.length
) {
attempt();
}
}, [isLoading, indexToLoad, attempt, components.length]);
const buttonRef = useRef<HTMLButtonElement>(null);
const resetRequestsAndRetry = useCallback(() => {
attemptsPerRequest.current = 0;
totalRequests.current = 0;
setState({ didReachMaximumRequests: false });
attempt();
}, [attempt, setState]);
const advance = useCallback(() => {
if (indexInView === undefined) {
setState({ indexInView: 0 });
} else if (
indexInView <= components.length - 1 && (
finalIndex === undefined ||
indexInView < finalIndex
)
) {
setState({ indexInView: indexInView + 1});
}
}, [components.length, finalIndex, indexInView, setState]);
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 renderMoreButton = () =>
<button
ref={buttonRef}
className="block w-full subtle"
onClick={didReachMaximumRequests ? resetRequestsAndRetry : advance}
disabled={isLoading}
>
{isLoading || !hasMounted
? <span className="relative inline-block translate-y-[3px]">
<Spinner size={16} />
</span>
: didReachMaximumRequests
? 'Try again …'
: label}
</button>;
if (debug) {
console.log({
indexInView,
componentsLength: components.length,
finalIndex,
hasFinalIndexBeenReached,
areAllComponentsVisible,
isLoading,
});
}
return <>
<div className="space-y-4">
<div className={itemsClass}>
{components.slice(0, (indexInView ?? 0) + 1)}
</div>
{showMoreButton && (
wrapMoreButtonInSiteGrid
? <SiteGrid contentMain={renderMoreButton()} />
: renderMoreButton()
)}
</div>
</>;
}

View File

@ -1,53 +0,0 @@
import MoreComponents from '@/components/MoreComponents';
import { getPhotosCached } from '@/photo/cache';
import PhotoGrid from './PhotoGrid';
import { useCallback } from 'react';
export function MorePhotosGrid({
initialOffset,
itemsPerRequest,
totalPhotosCount,
}: {
initialOffset: number
itemsPerRequest: number
totalPhotosCount: number
}) {
const getNextComponent = useCallback(async (
offset: number,
limit: number,
) => {
'use server';
if (
process.env.NODE_ENV === 'development' &&
Math.random() < 0.5
) {
return { didFail: true };
}
const photos = await getPhotosCached({ limit: offset + limit })
.catch(() => undefined);
if (!photos) {
return { didFail: true };
} else {
const nextPhotos = photos.slice(offset);
return {
...nextPhotos.length > 0 && {
nextComponent: <PhotoGrid photos={nextPhotos} />,
},
isFinished: offset + limit >= totalPhotosCount,
};
}
}, [totalPhotosCount]);
return (
initialOffset <= totalPhotosCount
? <MoreComponents
stateKey="PhotosGrid"
label="More photos"
itemsClass='space-y-0.5 sm:space-y-1'
initialOffset={initialOffset}
itemsPerRequest={itemsPerRequest}
getNextComponent={getNextComponent}
/>
: null
);
}

View File

@ -1,52 +0,0 @@
import MoreComponents from '@/components/MoreComponents';
import PhotosLarge from './PhotosLarge';
import { getPhotosCached } from '@/photo/cache';
import { useCallback } from 'react';
export function MorePhotosRoot({
initialOffset,
itemsPerRequest,
totalPhotosCount,
}: {
initialOffset: number
itemsPerRequest: number
totalPhotosCount: number
}) {
const getNextComponent = useCallback(async (
offset: number,
limit: number,
) => {
'use server';
if (
process.env.NODE_ENV === 'development' &&
Math.random() < 0.1
) {
return { didFail: true };
}
const photos = await getPhotosCached({ limit: offset + limit })
.catch(() => undefined);
if (!photos) {
return { didFail: true };
} else {
const nextPhotos = photos.slice(offset);
return {
nextComponent: <PhotosLarge photos={nextPhotos} />,
isFinished: offset + limit >= totalPhotosCount,
};
}
}, [totalPhotosCount]);
return (
initialOffset <= totalPhotosCount
? <MoreComponents
stateKey="PhotosRoot"
label="More photos"
initialOffset={initialOffset}
itemsPerRequest={itemsPerRequest}
getNextComponent={getNextComponent}
itemsClass="space-y-1"
wrapMoreButtonInSiteGrid
/>
:null
);
}

View File

@ -1,47 +0,0 @@
'use client';
import { ReactNode, useCallback, useState } from 'react';
import {
MoreComponentsContext,
MoreComponentsKey,
MoreComponentsState,
MORE_COMPONENTS_INITIAL_STATE,
MoreComponentsStateForKeyArgument,
} from './MoreComponentsState';
export default function MoreComponentsProvider({
children,
}: {
children: ReactNode
}) {
const [state, setState] =
useState<MoreComponentsState>(MORE_COMPONENTS_INITIAL_STATE);
const setStateForKey = useCallback((
key: MoreComponentsKey,
state: MoreComponentsStateForKeyArgument
) => {
setState(existingState => ({
...existingState,
...typeof state === 'function'
? { [key]: state(existingState[key]) }
: { [key]: { ...existingState[key], ...state } },
}));
}, []);
const clearMoreComponentsState = useCallback(() => {
setState(MORE_COMPONENTS_INITIAL_STATE);
}, []);
return (
<MoreComponentsContext.Provider
value={{
state,
setStateForKey,
clearMoreComponentsState,
}}
>
{children}
</MoreComponentsContext.Provider>
);
}

View File

@ -1,55 +0,0 @@
import { createContext, useContext } from 'react';
export type MoreComponentsKey =
'PhotosRoot' |
'PhotosGrid';
export interface MoreComponentsStateForKey {
hasMounted: boolean
isLoading: boolean
indexInView?: number
finalIndex?: number
didReachMaximumRequests: boolean
components: JSX.Element[]
}
export const createInitialStateForKey =
(): MoreComponentsStateForKey => ({
hasMounted: false,
isLoading: false,
didReachMaximumRequests: false,
components: [],
});
export type MoreComponentsState = Record<
MoreComponentsKey,
MoreComponentsStateForKey
>;
export type MoreComponentsStateForKeyArgument =
Partial<MoreComponentsStateForKey> |
((existingValue: MoreComponentsStateForKey) => MoreComponentsStateForKey);
export interface MoreComponentsContext {
state: MoreComponentsState
setStateForKey: (
key: MoreComponentsKey,
state: MoreComponentsStateForKeyArgument,
) => void
clearMoreComponentsState: () => void
}
export const MORE_COMPONENTS_INITIAL_STATE: MoreComponentsState = {
PhotosRoot: createInitialStateForKey(),
PhotosGrid: createInitialStateForKey(),
};
export const MoreComponentsContext =
createContext<MoreComponentsContext>({
state: MORE_COMPONENTS_INITIAL_STATE,
setStateForKey: () => {},
clearMoreComponentsState: () => {},
});
export const useMoreComponentsState = () =>
useContext(MoreComponentsContext);

View File

@ -1,13 +0,0 @@
'use client';
import { useCallback } from 'react';
import { useSWRConfig } from 'swr';
export default function useSwrClear() {
const { mutate } = useSWRConfig();
return useCallback(() => mutate(
_key => true,
undefined,
{ revalidate: false },
), [mutate]);
}