Remove legacy infinite scroll system
This commit is contained in:
parent
8ef0283822
commit
bd0f61f237
@ -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>
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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>
|
||||
</>;
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
@ -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]);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user