diff --git a/src/app/grid/page.tsx b/src/app/grid/page.tsx index 70263492..5d94e19e 100644 --- a/src/app/grid/page.tsx +++ b/src/app/grid/page.tsx @@ -55,8 +55,6 @@ export default async function GridPage() { } sideHiddenOnMobile /> - : - - + : ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3cd89156..8e2501b8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -96,8 +96,8 @@ export default function RootLayout({ - - + + diff --git a/src/components/AnimateChildren.tsx b/src/components/AnimateChildren.tsx new file mode 100644 index 00000000..7b558aa7 --- /dev/null +++ b/src/components/AnimateChildren.tsx @@ -0,0 +1,125 @@ +'use client'; + +import { Children, ReactNode, useRef } from 'react'; +import { Variant, motion } from 'framer-motion'; +import { useAppState } from '@/state/AppState'; +import usePrefersReducedMotion from '@/utility/usePrefersReducedMotion'; + +export type AnimationType = 'none' | 'scale' | 'left' | 'right' | 'bottom'; + +export interface AnimationConfig { + type?: AnimationType + duration?: number + staggerDelay?: number + scaleOffset?: number + distanceOffset?: number +} + +interface Props extends AnimationConfig { + children: ReactNode + className?: string + classNameItem?: string + animateFromAppState?: boolean + animateOnFirstLoadOnly?: boolean + staggerOnFirstLoadOnly?: boolean +} + +export default function AnimateChildren({ + children, + className, + classNameItem, + type = 'scale', + duration = 0.6, + staggerDelay = 0.1, + scaleOffset = 0.9, + distanceOffset = 20, + animateFromAppState, + animateOnFirstLoadOnly, + staggerOnFirstLoadOnly, +}: Props) { + const { + hasLoaded, + nextPhotoAnimation, + clearNextPhotoAnimation, + } = useAppState(); + + const prefersReducedMotion = usePrefersReducedMotion(); + + const hasLoadedInitial = useRef(hasLoaded); + const nextPhotoAnimationInitial = useRef(nextPhotoAnimation); + + const shouldAnimate = type !== 'none' && + !prefersReducedMotion && + !(animateOnFirstLoadOnly && hasLoadedInitial.current); + const shouldStagger = + !(staggerOnFirstLoadOnly && hasLoadedInitial.current); + + const typeResolved = animateFromAppState + ? (nextPhotoAnimationInitial.current?.type ?? type) + : type; + + const durationResolved = animateFromAppState + ? (nextPhotoAnimationInitial.current?.duration ?? duration) + : duration; + + const getInitialVariant = (): Variant => { + switch (typeResolved) { + case 'left': return { + opacity: 0, + transform: `translateX(${distanceOffset}px)`, + }; + case 'right': return { + opacity: 0, + transform: `translateX(${-distanceOffset}px)`, + }; + case 'bottom': return { + opacity: 0, + transform: `translateY(${distanceOffset}px)`, + }; + default: return { + opacity: 0, + transform: `translateY(${distanceOffset}px) scale(${scaleOffset})`, + }; + } + }; + + return ( + { + if (animateFromAppState) { + clearNextPhotoAnimation?.(); + } + }} + > + {Children.toArray(children).map((item, index) => + + {item} + )} + + ); +}; diff --git a/src/components/ErrorNote.tsx b/src/components/ErrorNote.tsx index 6adc07af..06f618bf 100644 --- a/src/components/ErrorNote.tsx +++ b/src/components/ErrorNote.tsx @@ -1,10 +1,11 @@ import { clsx } from 'clsx/lite'; +import { ReactNode } from 'react'; import { BiErrorAlt } from 'react-icons/bi'; export default function ErrorNote({ children, }: { - children: React.ReactNode + children: ReactNode }) { return (
indexToView - ) && ( - attemptsPerRequest.current < MAX_ATTEMPTS_PER_REQUEST && - totalRequests.current < MAX_TOTAL_REQUESTS - ); + const showMoreButton = + isLoading || + finalIndex === undefined || + finalIndex >= components.length; + + const currentTimeout = useRef(); const attempt = useCallback(() => { const handleError = () => { - setTimeout(() => { - attempt(); - }, RETRY_DELAY_IN_SECONDS * 1000); + 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 - 1) * itemsPerRequest, + initialOffset + indexToLoad * itemsPerRequest, itemsPerRequest, ) .then(({ nextComponent, isFinished, didFail }) => { - if (!didFail && nextComponent) { + if (!didFail) { + attemptsPerRequest.current = 0; setState(state => { const updatedComponents = [...state.components]; - updatedComponents[indexToLoad] = nextComponent; + if (nextComponent) { + updatedComponents[indexToLoad] = nextComponent; + } return { ...state, - components: updatedComponents, - indexLoaded: indexToLoad, + ...nextComponent && { components: updatedComponents}, + latestIndexLoaded: indexToLoad, isLoading: false, - ...isFinished && { lastIndexToLoad: indexToLoad }, + didReachMaximumRequests: false, + ...isFinished && { finalIndex: indexToLoad }, }; }); - attemptsPerRequest.current = 0; } else { handleError(); } @@ -104,7 +121,10 @@ export default function MoreComponents({ console.log( `Max total attempts reached (${MAX_TOTAL_REQUESTS})` ); - setState({ isLoading: false }); + setState({ + isLoading: false, + didReachMaximumRequests: true, + }); } } else { console.log( @@ -112,7 +132,7 @@ export default function MoreComponents({ ); setState({ isLoading: false, - haveAttemptsPerRequestBeenExceeded: true, + didReachMaximumRequests: true, }); } }, [ @@ -121,32 +141,37 @@ export default function MoreComponents({ initialOffset, indexToLoad, itemsPerRequest, + debug, ]); useEffect(() => { if ( !isLoading && - indexToLoad >= indexToView && - indexToLoad > indexLoaded + indexToLoad >= components.length ) { - console.log('Attempting', { isLoading }); attempt(); } - }, [ - isLoading, - indexToLoad, - indexToView, - indexLoaded, - attempt, - ]); + }, [isLoading, indexToLoad, indexInView, attempt, components.length]); const buttonRef = useRef(null); + const resetRequestsAndRetry = useCallback(() => { + attemptsPerRequest.current = 0; + totalRequests.current = 0; + setState({ didReachMaximumRequests: false }); + attempt(); + }, [attempt, setState]); + const advance = useCallback(() => { - if (indexToView <= indexLoaded) { - setState({ indexToView: indexToView + 1 }); + if (indexInView === undefined) { + setState({ indexInView: 0 }); + } else if ( + (indexInView <= components.length) && + (finalIndex === undefined || indexInView < finalIndex) + ) { + setState({ indexInView: indexInView + 1}); } - }, [setState, indexToView, indexLoaded]); + }, [components.length, finalIndex, indexInView, setState]); useEffect(() => { // Only add observer if button is rendered @@ -168,22 +193,26 @@ export default function MoreComponents({ ; return <>
-
{components.slice(0, indexToView + 1)}
- {(showMoreButton || true) && wrapMoreButtonInSiteGrid - ? - : renderMoreButton()} +
{components.slice(0, (indexInView ?? 0) + 1)}
+ {showMoreButton && ( + wrapMoreButtonInSiteGrid + ? + : renderMoreButton() + )}
; } diff --git a/src/photo/MorePhotosGrid.tsx b/src/photo/MorePhotosGrid.tsx index 846ce068..5db52fea 100644 --- a/src/photo/MorePhotosGrid.tsx +++ b/src/photo/MorePhotosGrid.tsx @@ -1,6 +1,7 @@ import MoreComponents from '@/components/MoreComponents'; import { getPhotosCached } from '@/photo/cache'; import PhotoGrid from './PhotoGrid'; +import { useCallback } from 'react'; export function MorePhotosGrid({ initialOffset, @@ -11,32 +12,38 @@ export function MorePhotosGrid({ 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: , + }, + isFinished: offset + limit >= totalPhotosCount, + }; + } + }, [totalPhotosCount]); return ( { - 'use server'; - if ( - process.env.NODE_ENV === 'development' && - Math.random() < 0.95 - ) { - 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: , - isFinished: offset + limit >= totalPhotosCount, - }; - } - }} + getNextComponent={getNextComponent} /> ); } diff --git a/src/photo/MorePhotosRoot.tsx b/src/photo/MorePhotosRoot.tsx index fe1b471b..dac48a0d 100644 --- a/src/photo/MorePhotosRoot.tsx +++ b/src/photo/MorePhotosRoot.tsx @@ -1,6 +1,7 @@ import MoreComponents from '@/components/MoreComponents'; import PhotosLarge from './PhotosLarge'; import { getPhotosCached } from '@/photo/cache'; +import { useCallback } from 'react'; export function MorePhotosRoot({ initialOffset, @@ -11,32 +12,36 @@ export function MorePhotosRoot({ 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 { + nextComponent: , + isFinished: offset + limit >= totalPhotosCount, + }; + } + }, [totalPhotosCount]); return ( { - 'use server'; - if ( - process.env.NODE_ENV === 'development' && - Math.random() < 0.95 - ) { - 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: , - isFinished: offset + limit >= totalPhotosCount, - }; - } - }} + getNextComponent={getNextComponent} wrapMoreButtonInSiteGrid /> ); diff --git a/src/photo/index.ts b/src/photo/index.ts index 8717aa01..b16a7b6b 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -17,7 +17,7 @@ export const GENERATE_STATIC_PARAMS_LIMIT = 1000; export const INFINITE_SCROLL_MULTIPLE_ROOT = process.env.NODE_ENV === 'development' ? 2 : 12; export const INFINITE_SCROLL_MULTIPLE_GRID = HIGH_DENSITY_GRID - ? process.env.NODE_ENV === 'development' ? 5 : 20 + ? process.env.NODE_ENV === 'development' ? 4 : 20 : process.env.NODE_ENV === 'development' ? 4 : 24; export const GRID_THUMBNAILS_TO_SHOW_MAX = 12; diff --git a/src/state/MoreComponentsState.ts b/src/state/MoreComponentsState.ts index d4cccb04..fb06deb5 100644 --- a/src/state/MoreComponentsState.ts +++ b/src/state/MoreComponentsState.ts @@ -5,20 +5,17 @@ export type MoreComponentsKey = 'PhotosGrid'; export interface MoreComponentsStateForKey { - indexToView: number - indexLoaded: number isLoading: boolean - lastIndexToLoad?: number - haveAttemptsPerRequestBeenExceeded: boolean + indexInView?: number + finalIndex?: number + didReachMaximumRequests: boolean components: JSX.Element[] } export const createInitialStateForKey = (): MoreComponentsStateForKey => ({ - indexToView: 0, - indexLoaded: 0, isLoading: false, - haveAttemptsPerRequestBeenExceeded: false, + didReachMaximumRequests: false, components: [], });