diff --git a/app/admin/components/page.tsx b/app/admin/components/page.tsx index 5dcd9ad5..55d79bad 100644 --- a/app/admin/components/page.tsx +++ b/app/admin/components/page.tsx @@ -1,8 +1,9 @@ import AdminComponentPageClient from '@/admin/AdminComponentPageClient'; +import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo'; import { getPhotosCached, getPhotosMetaCached } from '@/photo/cache'; export default async function ComponentsPage() { - const photos = await getPhotosCached({ limit: 50 }); + const photos = await getPhotosCached({ limit: INFINITE_SCROLL_GRID_INITIAL }); const photosCount = await getPhotosMetaCached() .then(({ count }) => count); diff --git a/src/about/AdminAboutEditPage.tsx b/src/about/AdminAboutEditPage.tsx index 0e79b3b2..ef54fba3 100644 --- a/src/about/AdminAboutEditPage.tsx +++ b/src/about/AdminAboutEditPage.tsx @@ -22,14 +22,12 @@ export default function AdminAboutEditPage({ photoHero: _photoHero, photos, photosCount, - photosHidden, }: { about?: About photoAvatar?: Photo photoHero?: Photo - photos?: Photo[] - photosCount?: number - photosHidden?: Photo[] + photos: Photo[] + photosCount: number shouldResizeImages?: boolean }) { const appText = useAppText(); @@ -73,7 +71,6 @@ export default function AdminAboutEditPage({ photo={photoAvatar} photos={photos} photosCount={photosCount} - photosHidden={photosHidden} /> ({ items, @@ -11,6 +12,7 @@ export default function SegmentMenu({ value: T icon?: ReactNode iconSelected?: ReactNode + isLoading?: boolean }[] selected: T onChange: (value: T) => void @@ -21,7 +23,7 @@ export default function SegmentMenu({ 'flex justify-center gap-1', className, )}> - {items.map(({ value, icon, iconSelected }) => ( + {items.map(({ value, icon, iconSelected, isLoading }) => ( ))} diff --git a/src/photo/InfinitePhotoScroll.tsx b/src/photo/InfinitePhotoScroll.tsx index e1063487..36d854d0 100644 --- a/src/photo/InfinitePhotoScroll.tsx +++ b/src/photo/InfinitePhotoScroll.tsx @@ -38,6 +38,7 @@ export default function InfinitePhotoScroll({ recipe, film, focal, + moreButtonClassName = 'mt-4', wrapMoreButtonInGrid, useCachedPhotos = true, includeHiddenPhotos, @@ -49,6 +50,7 @@ export default function InfinitePhotoScroll({ sortWithPriority?: boolean excludeFromFeeds?: boolean cacheKey: string + moreButtonClassName?: string wrapMoreButtonInGrid?: boolean useCachedPhotos?: boolean includeHiddenPhotos?: boolean @@ -178,7 +180,7 @@ export default function InfinitePhotoScroll({ revalidatePhoto, }) ))} - {!isFinished &&
+ {!isFinished &&
{wrapMoreButtonInGrid ? : renderMoreButton} diff --git a/src/photo/actions.ts b/src/photo/actions.ts index e4051f56..1c9d3d35 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -798,7 +798,7 @@ export const getPhotosCachedAction = async ( // Public actions -export const searchPhotosAction = async (query: string) => +export const searchPhotosPublicAction = async (query: string) => getPhotos({ query, limit: 10 }) .catch(e => { console.error('Could not query photos', e); diff --git a/src/photo/form/FieldsetPhotoChooser.tsx b/src/photo/form/FieldsetPhotoChooser.tsx index aae1b602..08012186 100644 --- a/src/photo/form/FieldsetPhotoChooser.tsx +++ b/src/photo/form/FieldsetPhotoChooser.tsx @@ -1,55 +1,55 @@ +/* eslint-disable react-hooks/set-state-in-effect */ import FieldsetWithStatus from '@/components/FieldsetWithStatus'; -import { altTextForPhoto, doesPhotoNeedBlurCompatibility, Photo } from '..'; +import { + altTextForPhoto, + doesPhotoNeedBlurCompatibility, + INFINITE_SCROLL_GRID_MULTIPLE, + Photo, +} from '..'; import clsx from 'clsx/lite'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import ImageMedium from '@/components/image/ImageMedium'; import { menuSurfaceStyles } from '@/components/primitives/surface'; -import { GRID_SPACE_CLASSNAME } from '@/components'; -import useDynamicPhoto from '../useDynamicPhoto'; import { IoSearch } from 'react-icons/io5'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import usePhotoQuery from '../usePhotoQuery'; -import Spinner from '@/components/Spinner'; import { BiChevronDown } from 'react-icons/bi'; import SegmentMenu from '@/components/SegmentMenu'; import IconFavs from '@/components/icons/IconFavs'; -import IconLock from '@/components/icons/IconLock'; +import InfinitePhotoScroll from '../InfinitePhotoScroll'; -type Mode = 'all' | 'favs' | 'hidden' | 'search'; +type Mode = 'all' | 'favs' | 'search'; -const renderPhoto = ({ - photo, - className, - onClick, -}: { - photo: Photo, - className?: string, - onClick?: () => void, -}) => +const CLASSNAME_GRID = 'grid grid-cols-3 gap-0.5'; + +const renderPhoto = (photo: Photo) => ; export default function FieldsetPhotoChooser({ label, value, onChange, - photo, + photo: _photo, photos = [], + photosCount, }: { label: string value: string - onChange: (value: string) => void + onChange: (photoId: string) => void photo?: Photo - photos?: Photo[] - photosCount?: number - photosHidden?: Photo[] + photos: Photo[] + photosCount: number }) { + const [isOpen, setIsOpen] = useState(false); + + const [photo, setPhoto] = useState(_photo); + const [mode, setMode] = useState('all'); const showQuery = mode === 'search'; @@ -59,42 +59,60 @@ export default function FieldsetPhotoChooser({ const [query, setQuery] = useState(''); const { photos: photosQuery, - isLoading, - reset, - } = usePhotoQuery(query); + isLoading: isLoadingPhotoQuery, + reset: resetPhotoQuery, + } = usePhotoQuery({ query, isPrivate: true }); - const { - photo: photoAvatar, - isLoading: isLoadingPhotoAvatar, - } = useDynamicPhoto({ - initialPhoto: photo, - photoId: value, - }); + const reset = useCallback((resetMenu?: boolean) => { + resetPhotoQuery(); + setQuery(''); + if (resetMenu) { setMode('all'); } + }, [resetPhotoQuery]); + // Focus input on query mode useEffect(() => { - if (showQuery) { - inputRef.current?.focus(); - } else { - reset(); - // eslint-disable-next-line react-hooks/set-state-in-effect - setQuery(''); - } - }, [showQuery, reset]); + if (showQuery) { inputRef.current?.focus(); } + }, [showQuery]); + + // Reset menu when closed + useEffect(() => { + if (!isOpen) { reset(true); } + }, [isOpen, reset]); + + const renderPhotoButton = (photo: Photo) => + { + setPhoto(photo); + onChange(photo.id); + setIsOpen(false); + }} + > + {renderPhoto(photo)} + ; return ( <> - + - @@ -115,59 +130,65 @@ export default function FieldsetPhotoChooser({ onCloseAutoFocus={e => e.preventDefault()} align="start" sideOffset={10} - className={menuSurfaceStyles('z-20 px-1.5 py-1.5 rounded-2xl')} + className={menuSurfaceStyles('z-20 rounded-2xl pb-0 overflow-auto')} > + , + iconSelected: , + }, { + value: 'search', + icon: , + isLoading: isLoadingPhotoQuery, + }]} + selected={mode} + onChange={mode => { + setMode(mode); + if (mode !== 'search') { + reset(); + } + }} + />
- , - iconSelected: , - }, { - value: 'hidden', - icon: , - }, { - value: 'search', - icon: isLoading - ? - : , - }]} - selected={mode} - onChange={setMode} + setQuery(e.target.value)} /> - {showQuery && - setQuery(e.target.value)} - />} -
- {(showQuery && query ? photosQuery : photos).map(photo => ( - - {renderPhoto({ - photo, - onClick: () => onChange(photo.id), - })} - - ))} +
+
+
+ {(showQuery && query ? photosQuery : photos) + .map(photo => renderPhotoButton(photo))}
+ {!(showQuery && query) && photosCount > photos.length && + + {({ key, photos }) => ( +
+ {photos.map(photo => renderPhotoButton(photo))} +
+ )} +
}
diff --git a/src/photo/usePhotoQuery.ts b/src/photo/usePhotoQuery.ts index eadfba60..eeb425bd 100644 --- a/src/photo/usePhotoQuery.ts +++ b/src/photo/usePhotoQuery.ts @@ -2,16 +2,22 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { Photo } from '.'; import { useDebounce } from 'use-debounce'; -import { searchPhotosAction } from './actions'; +import { getPhotosAction, searchPhotosPublicAction } from './actions'; const formatQuery = (query: string) => query.trim().toLocaleLowerCase(); -export default function usePhotoQuery( - query: string, +export default function usePhotoQuery({ + query, isEnabled = true, minimumQueryLength = 2, -) { + isPrivate, +}: { + query: string + isEnabled?: boolean + minimumQueryLength?: number + isPrivate?: boolean +}) { const [isLoading, setIsLoading] = useState(false); const queryFormatted = useMemo(() => @@ -30,7 +36,9 @@ export default function usePhotoQuery( useEffect(() => { if (queryDebounced.length >= minimumQueryLength && isEnabled) { setIsLoading(true); - searchPhotosAction(queryDebounced) + (isPrivate + ? getPhotosAction({ query: queryDebounced }) + : searchPhotosPublicAction(queryDebounced)) .then(setPhotos) .finally(() => setIsLoading(false)); } @@ -38,6 +46,7 @@ export default function usePhotoQuery( queryDebounced, minimumQueryLength, isEnabled, + isPrivate, ]); useEffect(() => {