diff --git a/app/about/edit/page.tsx b/app/about/edit/page.tsx
index 62475055..c02bbdd6 100644
--- a/app/about/edit/page.tsx
+++ b/app/about/edit/page.tsx
@@ -1,26 +1,70 @@
import AdminAboutEditPage from '@/about/AdminAboutEditPage';
import { getAbout } from '@/about/query';
import { PRESERVE_ORIGINAL_UPLOADS } from '@/app/config';
-import { getPhotoNoStore } from '@/photo/cache';
+import { feedQueryOptions } from '@/feed';
+import {
+ getPhotosCached,
+ getPhotosMetaCached,
+} from '@/photo/cache';
+import { getPhoto } from '@/photo/query';
+import { TAG_FAVS } from '@/tag';
+
+const PHOTO_CHOOSER_QUERY_OPTIONS = feedQueryOptions({
+ isGrid: true,
+ excludeFromFeeds: false,
+});
export default async function AboutEditPage() {
- const about = await getAbout().catch(() => undefined);
+ const [
+ {
+ about,
+ photoAvatar,
+ photoHero,
+ },
+ photos,
+ photosCount,
+ photosFavs,
+ ] = await Promise.all([
+ getAbout()
+ .then(async about => {
+ const photoAvatar = about?.photoIdAvatar
+ ? await getPhoto(about?.photoIdAvatar ?? '', true)
+ .catch(() => undefined)
+ : undefined;
- const photoAvatar = about?.photoIdAvatar
- ? await getPhotoNoStore(about?.photoIdAvatar ?? '', true)
- .catch(() => undefined)
- : undefined;
+ const photoHero = about?.photoIdHero
+ ? await getPhoto(about?.photoIdHero ?? '', true)
+ .catch(() => undefined)
+ : undefined;
- const photoHero = about?.photoIdHero
- ? await getPhotoNoStore(about?.photoIdHero ?? '', true)
- .catch(() => undefined)
- : undefined;
+ return {
+ about,
+ photoAvatar,
+ photoHero,
+ };
+ })
+ .catch(() => ({
+ about: undefined,
+ photoAvatar: undefined,
+ photoHero: undefined,
+ })),
+ getPhotosCached(PHOTO_CHOOSER_QUERY_OPTIONS)
+ .catch(() => []),
+ getPhotosMetaCached(PHOTO_CHOOSER_QUERY_OPTIONS)
+ .then(({ count }) => count)
+ .catch(() => 0),
+ getPhotosCached({ tag: TAG_FAVS })
+ .catch(() => []),
+ ]);
return (
);
diff --git a/app/admin/components/page.tsx b/app/admin/components/page.tsx
index 9869f3ff..a395c543 100644
--- a/app/admin/components/page.tsx
+++ b/app/admin/components/page.tsx
@@ -1,10 +1,20 @@
import AdminComponentPageClient from '@/admin/AdminComponentPageClient';
-import { getPhotosCached } from '@/photo/cache';
+import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
+import { getPhotosCached, getPhotosMetaCached } from '@/photo/cache';
+import { TAG_FAVS } from '@/tag';
export default async function ComponentsPage() {
- const photos = await getPhotosCached({ limit: 1});
+ const photos = await getPhotosCached({ limit: INFINITE_SCROLL_GRID_INITIAL });
+ const photosCount = await getPhotosMetaCached()
+ .then(({ count }) => count);
+ const photosFavs = await getPhotosCached({ tag: TAG_FAVS });
return (
-
+
);
}
diff --git a/app/full/[sortType]/[sortOrder]/page.tsx b/app/full/[sortType]/[sortOrder]/page.tsx
index ec60ea4d..245ccaaa 100644
--- a/app/full/[sortType]/[sortOrder]/page.tsx
+++ b/app/full/[sortType]/[sortOrder]/page.tsx
@@ -8,12 +8,12 @@ import { getPhotosMetaCached } from '@/photo/cache';
import { SortProps } from '@/photo/sort';
import { getSortOptionsFromParams } from '@/photo/sort/path';
import { PhotoQueryOptions } from '@/db';
-import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
+import { FEED_META_QUERY_OPTIONS, feedQueryOptions } from '@/feed';
export const maxDuration = 60;
const getPhotosCached = cache((options: PhotoQueryOptions) =>
- getPhotos(getFeedQueryOptions({
+ getPhotos(feedQueryOptions({
isGrid: false,
...options,
})));
diff --git a/app/full/page.tsx b/app/full/page.tsx
index c3e17380..5dca8262 100644
--- a/app/full/page.tsx
+++ b/app/full/page.tsx
@@ -6,12 +6,12 @@ import { getPhotos } from '@/photo/query';
import PhotoFullPage from '@/photo/PhotoFullPage';
import { getPhotosMetaCached } from '@/photo/cache';
import { USER_DEFAULT_SORT_OPTIONS } from '@/app/config';
-import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
+import { FEED_META_QUERY_OPTIONS, feedQueryOptions } from '@/feed';
export const dynamic = 'force-static';
export const maxDuration = 60;
-const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({
+const getPhotosCached = cache(() => getPhotos(feedQueryOptions({
isGrid: false,
})));
diff --git a/app/grid/[sortType]/[sortOrder]/page.tsx b/app/grid/[sortType]/[sortOrder]/page.tsx
index e332d7ef..041e7794 100644
--- a/app/grid/[sortType]/[sortOrder]/page.tsx
+++ b/app/grid/[sortType]/[sortOrder]/page.tsx
@@ -8,13 +8,13 @@ import { getDataForCategoriesCached } from '@/category/cache';
import { getPhotosMetaCached } from '@/photo/cache';
import { SortProps } from '@/photo/sort';
import { getSortOptionsFromParams } from '@/photo/sort/path';
-import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
+import { FEED_META_QUERY_OPTIONS, feedQueryOptions } from '@/feed';
import { PhotoQueryOptions } from '@/db';
export const maxDuration = 60;
const getPhotosCached = cache((options: PhotoQueryOptions) =>
- getPhotos(getFeedQueryOptions({
+ getPhotos(feedQueryOptions({
isGrid: true,
...options,
})));
diff --git a/app/grid/page.tsx b/app/grid/page.tsx
index ba6fe55f..b64512ef 100644
--- a/app/grid/page.tsx
+++ b/app/grid/page.tsx
@@ -7,12 +7,12 @@ import PhotoGridPage from '@/photo/PhotoGridPage';
import { getDataForCategoriesCached } from '@/category/cache';
import { getPhotosMetaCached } from '@/photo/cache';
import { USER_DEFAULT_SORT_OPTIONS } from '@/app/config';
-import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
+import { FEED_META_QUERY_OPTIONS, feedQueryOptions } from '@/feed';
export const dynamic = 'force-static';
export const maxDuration = 60;
-const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({
+const getPhotosCached = cache(() => getPhotos(feedQueryOptions({
isGrid: true,
})));
diff --git a/app/page.tsx b/app/page.tsx
index f954497d..5c062cd7 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -9,12 +9,12 @@ import PhotoFullPage from '@/photo/PhotoFullPage';
import PhotoGridPage from '@/photo/PhotoGridPage';
import { getDataForCategoriesCached } from '@/category/cache';
import { getPhotosMetaCached } from '@/photo/cache';
-import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
+import { FEED_META_QUERY_OPTIONS, feedQueryOptions } from '@/feed';
export const dynamic = 'force-static';
export const maxDuration = 60;
-const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({
+const getPhotosCached = cache(() => getPhotos(feedQueryOptions({
isGrid: GRID_HOMEPAGE_ENABLED,
})));
diff --git a/src/about/AdminAboutEditPage.tsx b/src/about/AdminAboutEditPage.tsx
index 79ba4d56..739c3f3f 100644
--- a/src/about/AdminAboutEditPage.tsx
+++ b/src/about/AdminAboutEditPage.tsx
@@ -14,15 +14,22 @@ import PhotoMedium from '@/photo/PhotoMedium';
import clsx from 'clsx/lite';
import useDynamicPhoto from '@/photo/useDynamicPhoto';
import { useAppText } from '@/i18n/state/client';
+import FieldsetPhotoChooser from '@/photo/form/FieldsetPhotoChooser';
export default function AdminAboutEditPage({
about,
photoAvatar: _photoAvatar,
photoHero: _photoHero,
+ photos,
+ photosCount,
+ photosFavs,
}: {
about?: About
photoAvatar?: Photo
photoHero?: Photo
+ photos: Photo[]
+ photosCount: number
+ photosFavs: Photo[]
shouldResizeImages?: boolean
}) {
const appText = useAppText();
@@ -58,6 +65,16 @@ export default function AdminAboutEditPage({
action={updateAboutAction}
>
+
setAboutForm(form =>
+ ({ ...form, photoIdAvatar }))}
+ photo={photoAvatar}
+ photos={photos}
+ photosCount={photosCount}
+ photosFavs={photosFavs}
+ />
-
-
-
diff --git a/src/cmdk/CommandKClient.tsx b/src/cmdk/CommandKClient.tsx
index 1fedb886..7abbd5d8 100644
--- a/src/cmdk/CommandKClient.tsx
+++ b/src/cmdk/CommandKClient.tsx
@@ -37,14 +37,12 @@ import {
} from '../app/path';
import Modal from '../components/Modal';
import { clsx } from 'clsx/lite';
-import { useDebounce } from 'use-debounce';
import Spinner from '../components/Spinner';
import { usePathname, useRouter } from 'next/navigation';
import { useTheme } from 'next-themes';
import { BiDesktop, BiLockAlt, BiMoon, BiSun } from 'react-icons/bi';
import { IoClose, IoInvertModeSharp } from 'react-icons/io5';
import { useAppState } from '@/app/AppState';
-import { searchPhotosAction } from '@/photo/actions';
import { RiToolsFill } from 'react-icons/ri';
import { signOutAction } from '@/auth/actions';
import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
@@ -98,12 +96,12 @@ import { getSortStateFromPath } from '@/photo/sort/path';
import IconSort from '@/components/icons/IconSort';
import { useSelectPhotosState } from '@/admin/select/SelectPhotosState';
import IconAlbum from '@/components/icons/IconAlbum';
+import usePhotoQuery from '@/photo/usePhotoQuery';
const DIALOG_TITLE = 'Global Command-K Menu';
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
const LISTENER_KEYDOWN = 'keydown';
-const MINIMUM_QUERY_LENGTH = 2;
const MAX_HEIGHT = '20rem';
@@ -246,19 +244,13 @@ export default function CommandKClient({
}
}, [isWaiting, setIsOpen]);
- // Raw query values
- const [queryLiveRaw, setQueryLiveRaw] = useState('');
- const [queryDebouncedRaw] =
- useDebounce(queryLiveRaw, 500, { trailing: true });
-
- // Parameterized query values
- const queryLive = useMemo(() =>
- queryLiveRaw.trim().toLocaleLowerCase(), [queryLiveRaw]);
- const queryDebounced = useMemo(() =>
- queryDebouncedRaw.trim().toLocaleLowerCase(), [queryDebouncedRaw]);
-
- const [isLoading, setIsLoading] = useState(false);
- const [queriedSections, setQueriedSections] = useState
([]);
+ const [query, setQuery] = useState('');
+ const {
+ queryFormatted,
+ photos,
+ isLoading,
+ reset,
+ } = usePhotoQuery({ query, isEnabled: !isPending });
const { setTheme } = useTheme();
@@ -283,55 +275,32 @@ export default function CommandKClient({
return () => document.removeEventListener(LISTENER_KEYDOWN, down);
}, [setIsOpen]);
- useEffect(() => {
- if (queryDebounced.length >= MINIMUM_QUERY_LENGTH && !isPending) {
- setIsLoading(true);
- searchPhotosAction(queryDebounced)
- .then(photos => {
- if (isOpenRef.current) {
- setQueriedSections(photos.length > 0
- ? [{
- heading: 'Photos',
- accessory: ,
- items: photos.map(photo => ({
- label: titleForPhoto(photo),
- keywords: getKeywordsForPhoto(photo),
- annotation: ,
- accessory: ,
- path: pathForPhoto({ photo }),
- })),
- }]
- : []);
- } else {
- // Ignore stale requests that come in after dialog is closed
- setQueriedSections([]);
- }
- setIsLoading(false);
- })
- .catch(e => {
- console.error(e);
- setQueriedSections([]);
- setIsLoading(false);
- });
+ const queriedSections = useMemo(() => {
+ if (isOpenRef.current && photos.length > 0) {
+ return [{
+ heading: 'Photos',
+ accessory: ,
+ items: photos.map(photo => ({
+ label: titleForPhoto(photo),
+ keywords: getKeywordsForPhoto(photo),
+ annotation: ,
+ accessory: ,
+ path: pathForPhoto({ photo }),
+ })),
+ }];
+ } else {
+ return [];
}
- }, [queryDebounced, isPending, appText]);
-
- useEffect(() => {
- if (queryLive === '') {
- setQueriedSections([]);
- setIsLoading(false);
- } else if (queryLive.length >= MINIMUM_QUERY_LENGTH) {
- setIsLoading(true);
- }
- }, [queryLive]);
+ },
+ [photos],
+ );
useEffect(() => {
if (!isOpen) {
- setQueryLiveRaw('');
- setQueriedSections([]);
- setIsLoading(false);
+ setQuery('');
+ reset();
}
- }, [isOpen]);
+ }, [isOpen, reset]);
const recent = recents[0];
const recentsStatus = useMemo(() => {
@@ -345,17 +314,17 @@ export default function CommandKClient({
// Years only accessible by search
const years = useMemo(() =>
- _years.filter(({ year }) => queryLive && year.includes(queryLive))
- , [_years, queryLive]);
+ _years.filter(({ year }) => queryFormatted && year.includes(queryFormatted))
+ , [_years, queryFormatted]);
const tags = useMemo(() => {
const tagsIncludingPrivate = photosCountHidden > 0
? addPrivateToTags(_tags, photosCountHidden)
: _tags;
return HIDE_TAGS_WITH_ONE_PHOTO
- ? limitTagsByCount(tagsIncludingPrivate, 2, queryLive)
+ ? limitTagsByCount(tagsIncludingPrivate, 2, queryFormatted)
: tagsIncludingPrivate;
- }, [_tags, photosCountHidden, queryLive]);
+ }, [_tags, photosCountHidden, queryFormatted]);
const categorySections: CommandKSection[] = useMemo(() =>
CATEGORY_VISIBILITY
@@ -751,9 +720,9 @@ export default function CommandKClient({
)}>
{
- setQueryLiveRaw(value);
+ setQuery(value);
updateMask();
}}
className={clsx(
@@ -782,15 +751,15 @@ export default function CommandKClient({
'text-gray-400/90 dark:text-gray-700',
)}
onClick={() => {
- if (queryLiveRaw) {
- setQueryLiveRaw('');
+ if (query) {
+ setQuery('');
updateMask();
} else {
setIsOpen?.(false);
}
}}
>
- {queryLiveRaw
+ {query
?
: <>
@@ -889,7 +858,7 @@ export default function CommandKClient({
/>;
})}
)}
- {footer && !queryLive &&
+ {footer && !queryFormatted &&
({
+ items,
+ selected,
+ onChange,
+ className,
+}: {
+ items: {
+ value: T
+ icon?: ReactNode
+ iconSelected?: ReactNode
+ isLoading?: boolean
+ }[]
+ selected: T
+ onChange: (value: T) => void
+ className?: string
+}) {
+ return (
+
+ {items.map(({ value, icon, iconSelected, isLoading }) => (
+
+ ))}
+
+ );
+}
diff --git a/src/components/more/MoreMenu.tsx b/src/components/more/MoreMenu.tsx
index b1dd6f79..e02e4056 100644
--- a/src/components/more/MoreMenu.tsx
+++ b/src/components/more/MoreMenu.tsx
@@ -11,21 +11,7 @@ import { FiMoreHorizontal } from 'react-icons/fi';
import MoreMenuItem from './MoreMenuItem';
import { clearGlobalFocus } from '@/utility/dom';
import { FaChevronRight } from 'react-icons/fa6';
-
-const surfaceStyles = (className?: string) => clsx(
- 'z-10',
- 'min-w-[8rem]',
- 'component-surface',
- 'py-1',
- 'not-dark:shadow-lg not-dark:shadow-gray-900/10',
- 'data-[side=top]:dark:shadow-[0_0px_40px_rgba(0,0,0,0.6)]',
- 'data-[side=bottom]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
- 'data-[side=right]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
- 'data-[side=top]:animate-fade-in-from-bottom',
- 'data-[side=bottom]:animate-fade-in-from-top',
- 'data-[side=right]:animate-fade-in-from-top',
- className,
-);
+import { MENU_SURFACE_STYLES } from '../primitives/surface';
export type MoreMenuSection = {
label?: string
@@ -115,7 +101,10 @@ export default function MoreMenu({
onCloseAutoFocus={e => e.preventDefault()}
align={align}
sideOffset={sideOffset}
- className={surfaceStyles(className)}
+ className={clsx(
+ MENU_SURFACE_STYLES,
+ className,
+ )}
>
{header &&
{item.items.map(item =>
diff --git a/src/components/primitives/TooltipPrimitive.tsx b/src/components/primitives/TooltipPrimitive.tsx
index 4d7c652f..f090331a 100644
--- a/src/components/primitives/TooltipPrimitive.tsx
+++ b/src/components/primitives/TooltipPrimitive.tsx
@@ -2,7 +2,7 @@
import { ReactNode, useRef, useState, ComponentProps } from 'react';
import * as Tooltip from '@radix-ui/react-tooltip';
-import MenuSurface from './MenuSurface';
+import ComponentSurface from './surface/ComponentSurface';
import clsx from 'clsx/lite';
import useClickInsideOutside from '@/utility/useClickInsideOutside';
import KeyCommand from './KeyCommand';
@@ -31,7 +31,7 @@ export default function TooltipPrimitive({
children: ReactNode
className?: string
classNameTrigger?: string
- color?: ComponentProps['color']
+ color?: ComponentProps['color']
keyCommand?: string
keyCommandModifier?: ComponentProps['modifier']
supportMobile?: boolean
@@ -126,9 +126,9 @@ export default function TooltipPrimitive({
)}
>
{content &&
-
+
{content}
- }
+ }
diff --git a/src/components/primitives/MenuSurface.tsx b/src/components/primitives/surface/ComponentSurface.tsx
similarity index 94%
rename from src/components/primitives/MenuSurface.tsx
rename to src/components/primitives/surface/ComponentSurface.tsx
index f34480f1..5c339d06 100644
--- a/src/components/primitives/MenuSurface.tsx
+++ b/src/components/primitives/surface/ComponentSurface.tsx
@@ -1,7 +1,7 @@
import { ReactNode, RefObject } from 'react';
import clsx from 'clsx/lite';
-export default function MenuSurface({
+export default function ComponentSurface({
ref,
children,
className,
diff --git a/src/components/primitives/surface/index.ts b/src/components/primitives/surface/index.ts
new file mode 100644
index 00000000..183083e1
--- /dev/null
+++ b/src/components/primitives/surface/index.ts
@@ -0,0 +1,15 @@
+import clsx from 'clsx/lite';
+
+export const MENU_SURFACE_STYLES = clsx(
+ 'z-10',
+ 'min-w-[8rem]',
+ 'component-surface',
+ 'py-1',
+ 'not-dark:shadow-lg not-dark:shadow-gray-900/10',
+ 'data-[side=top]:dark:shadow-[0_0px_40px_rgba(0,0,0,0.6)]',
+ 'data-[side=bottom]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
+ 'data-[side=right]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
+ 'data-[side=top]:animate-fade-in-from-bottom',
+ 'data-[side=bottom]:animate-fade-in-from-top',
+ 'data-[side=right]:animate-fade-in-from-top',
+);
diff --git a/src/components/shared-hover/SharedHoverProvider.tsx b/src/components/shared-hover/SharedHoverProvider.tsx
index 837eb526..d089e7d6 100644
--- a/src/components/shared-hover/SharedHoverProvider.tsx
+++ b/src/components/shared-hover/SharedHoverProvider.tsx
@@ -10,7 +10,7 @@ import {
} from 'react';
import { SharedHoverContext, SharedHoverProps } from './state';
import { AnimatePresence, motion } from 'framer-motion';
-import MenuSurface from '../primitives/MenuSurface';
+import ComponentSurface from '../primitives/surface/ComponentSurface';
import clsx from 'clsx/lite';
const WINDOW_CHANGE_EVENTS = ['mouseup', 'mousewheel', 'resize'];
@@ -133,7 +133,7 @@ export default function SharedHoverProvider({
className="fixed"
style={hoverStyle}
>
-
@@ -158,7 +158,7 @@ export default function SharedHoverProvider({
: 'border-medium',
)} />
-
+
}
diff --git a/src/components/shared-hover/state.ts b/src/components/shared-hover/state.ts
index 50fa08cb..b9d95778 100644
--- a/src/components/shared-hover/state.ts
+++ b/src/components/shared-hover/state.ts
@@ -6,7 +6,7 @@ import {
SetStateAction,
use,
} from 'react';
-import MenuSurface from '../primitives/MenuSurface';
+import ComponentSurface from '../primitives/surface/ComponentSurface';
export type SharedHoverProps = {
key: string
@@ -14,7 +14,7 @@ export type SharedHoverProps = {
height: number
offsetAbove: number
offsetBelow: number
- color?: ComponentProps
['color']
+ color?: ComponentProps['color']
}
export type SharedHoverState = {
diff --git a/src/feed/index.ts b/src/feed/index.ts
index 059ad6d8..1c5f662e 100644
--- a/src/feed/index.ts
+++ b/src/feed/index.ts
@@ -13,21 +13,23 @@ const FEED_BASE_QUERY_OPTIONS: PhotoQueryOptions = {
// PAGE FEED QUERY OPTIONS
-export const getFeedQueryOptions = ({
+export const feedQueryOptions = ({
isGrid,
sortBy = USER_DEFAULT_SORT_OPTIONS.sortBy,
sortWithPriority = USER_DEFAULT_SORT_OPTIONS.sortWithPriority,
+ ...options
}: {
isGrid: boolean,
sortBy?: SortBy,
sortWithPriority?: boolean,
-}): PhotoQueryOptions => ({
+} & PhotoQueryOptions): PhotoQueryOptions => ({
...FEED_BASE_QUERY_OPTIONS,
sortBy,
sortWithPriority,
limit: isGrid
? INFINITE_SCROLL_GRID_INITIAL
: INFINITE_SCROLL_FULL_INITIAL,
+ ...options,
});
export const FEED_META_QUERY_OPTIONS: PhotoQueryOptions = {
diff --git a/src/photo/FieldsetPhotoChooser.tsx b/src/photo/FieldsetPhotoChooser.tsx
deleted file mode 100644
index e841db54..00000000
--- a/src/photo/FieldsetPhotoChooser.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import FieldsetWithStatus from '@/components/FieldsetWithStatus';
-import { altTextForPhoto, doesPhotoNeedBlurCompatibility, Photo } from '.';
-import clsx from 'clsx/lite';
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
-import ImageMedium from '@/components/image/ImageMedium';
-import PhotoGridInfinite from './PhotoGridInfinite';
-
-export default function FieldsetPhotoChooser({
- label,
- value,
- onChange,
- photo,
-}: {
- label: string
- value: string
- onChange: (value: string) => void
- photo?: Photo
-}) {
- return (
- <>
-
-
-
-
-
-
- e.preventDefault()}
- align="start"
- sideOffset={10}
- // alignOffset={-10}
- className={clsx(
- 'z-20',
- 'min-w-[8rem]',
- 'component-surface',
- 'p-1.5',
- 'not-dark:shadow-lg not-dark:shadow-gray-900/10',
- 'data-[side=top]:dark:shadow-[0_0px_40px_rgba(0,0,0,0.6)]',
- 'data-[side=bottom]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
- 'data-[side=right]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
- 'data-[side=top]:animate-fade-in-from-bottom',
- 'data-[side=bottom]:animate-fade-in-from-top',
- 'data-[side=right]:animate-fade-in-from-top',
- )}>
-
-
-
-
- >
- );
-}
\ No newline at end of file
diff --git a/src/photo/FieldsetPhotoQuery.tsx b/src/photo/FieldsetPhotoQuery.tsx
deleted file mode 100644
index 80138cf9..00000000
--- a/src/photo/FieldsetPhotoQuery.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-'use client';
-
-import FieldsetWithStatus from '@/components/FieldsetWithStatus';
-import { Photo } from '.';
-import { useEffect, useState } from 'react';
-import { AnnotatedTag } from './form';
-import { useDebounce } from 'use-debounce';
-import PhotoSmall from './PhotoSmall';
-import { getPhotosAction } from './actions';
-
-const convertPhotoToAnnotatedTag = (photo: Photo): AnnotatedTag => ({
- value: photo.id,
- label: photo.title,
- icon: ,
-});
-
-export default function FieldsetPhotoQuery({
- label,
- photos = [],
- value,
- onChange,
-}: {
- label: string
- photos?: Photo[]
- value: string
- onChange: (value: string) => void
-}) {
- const [query, setQuery] = useState('');
- const [queryDebounced] = useDebounce(query, 500);
- const [isQuerying, setIsQuerying] = useState(false);
-
- const [photoOptions, setPhotoOptions] = useState(photos
- .map(convertPhotoToAnnotatedTag),
- );
-
- useEffect(() => {
- if (queryDebounced) {
- // eslint-disable-next-line react-hooks/set-state-in-effect
- setIsQuerying(true);
- getPhotosAction({ query: queryDebounced })
- .then(photos => {
- setPhotoOptions(photos.map(convertPhotoToAnnotatedTag));
- })
- .finally(() => {
- setIsQuerying(false);
- });
- } else {
- setPhotoOptions([]);
- }
- }, [queryDebounced]);
-
- return (
-
- photoOptions.find(option => option.value === value)?.label}
- tagOptionsAllowNewValues={false}
- tagOptionsShouldParameterize={false}
- tagOptionsLimit={1}
- loading={isQuerying}
- />
- );
-}
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
new file mode 100644
index 00000000..c4154924
--- /dev/null
+++ b/src/photo/form/FieldsetPhotoChooser.tsx
@@ -0,0 +1,243 @@
+/* eslint-disable react-hooks/set-state-in-effect */
+import FieldsetWithStatus from '@/components/FieldsetWithStatus';
+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 { MENU_SURFACE_STYLES } from '@/components/primitives/surface';
+import { IoSearch } from 'react-icons/io5';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import usePhotoQuery from '../usePhotoQuery';
+import { BiChevronRight } from 'react-icons/bi';
+import SegmentMenu from '@/components/SegmentMenu';
+import IconFavs from '@/components/icons/IconFavs';
+import InfinitePhotoScroll from '../InfinitePhotoScroll';
+import AdminEmptyState from '@/admin/AdminEmptyState';
+import { TbPhotoSearch } from 'react-icons/tb';
+
+type Mode = 'all' | 'favs' | 'search';
+
+const CLASSNAME_GRID = 'grid grid-cols-3 gap-0.5';
+
+const renderPhoto = (photo: Photo) =>
+
;
+
+export default function FieldsetPhotoChooser({
+ label,
+ value,
+ onChange,
+ photo: _photo,
+ photos = [],
+ photosCount,
+ photosFavs,
+}: {
+ label: string
+ value: string
+ onChange: (photoId: string) => void
+ photo?: Photo
+ photos: Photo[]
+ photosCount: number
+ photosFavs: Photo[]
+}) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const [photo, setPhoto] = useState(_photo);
+
+ const [mode, setMode] = useState
('all');
+
+ const showQuery = mode === 'search';
+
+ const inputRef = useRef(null);
+
+ const [query, setQuery] = useState('');
+ const {
+ photos: photosQuery,
+ isLoading: isLoadingPhotoQuery,
+ reset: resetPhotoQuery,
+ resultsNotFound,
+ } = usePhotoQuery({ query, isPrivate: true });
+
+ const reset = useCallback((resetMenu?: boolean) => {
+ resetPhotoQuery();
+ setQuery('');
+ if (resetMenu) { setMode('all'); }
+ }, [resetPhotoQuery]);
+
+ // Focus input on query mode
+ useEffect(() => {
+ 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)}
+ ;
+
+ const photosToShow = showQuery && query
+ ? photosQuery
+ : mode === 'favs'
+ ? photosFavs : photos;
+
+ const shouldPaginate =
+ !(showQuery && query) &&
+ photosCount > photos.length &&
+ mode !== 'favs';
+
+ return (
+ <>
+
+
+
+
+
+
+ e.preventDefault()}
+ align="start"
+ sideOffset={-80}
+ className={clsx(
+ MENU_SURFACE_STYLES,
+ 'z-20 rounded-2xl pb-0 overflow-auto',
+ )}
+ >
+ ,
+ iconSelected: ,
+ }, {
+ value: 'search',
+ icon: ,
+ isLoading: isLoadingPhotoQuery,
+ }]}
+ selected={mode}
+ onChange={mode => {
+ setMode(mode);
+ if (mode !== 'search') {
+ reset();
+ }
+ }}
+ />
+
+
+
+ setQuery(e.target.value)}
+ />
+
+
+ {showQuery && resultsNotFound &&
+
}
+ className="translate-y-8"
+ includeContainer={false}
+ >
+ No photos found
+ }
+ {!showQuery && photosToShow.length === 0 &&
+
}
+ className="translate-y-16"
+ includeContainer={false}
+ >
+ No photos
+ }
+
+ {photosToShow.map(photo => renderPhotoButton(photo))}
+
+ {shouldPaginate &&
+
+ {({ key, photos }) => (
+
+ {photos.map(photo => renderPhotoButton(photo))}
+
+ )}
+ }
+
+
+
+
+ >
+ );
+}
diff --git a/src/photo/useDynamicPhoto.ts b/src/photo/useDynamicPhoto.ts
index ea4963ce..b3f36a8a 100644
--- a/src/photo/useDynamicPhoto.ts
+++ b/src/photo/useDynamicPhoto.ts
@@ -14,7 +14,7 @@ export default function useDynamicPhoto({
const [isLoading, setIsLoading] = useState(false);
- const [photoIdDebounced] = useDebounce(photoId, 500);
+ const [photoIdDebounced] = useDebounce(photoId, 500, { leading: true });
useEffect(() => {
if (photoIdDebounced) {
diff --git a/src/photo/usePhotoQuery.ts b/src/photo/usePhotoQuery.ts
new file mode 100644
index 00000000..52091c16
--- /dev/null
+++ b/src/photo/usePhotoQuery.ts
@@ -0,0 +1,73 @@
+/* eslint-disable react-hooks/set-state-in-effect */
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { Photo } from '.';
+import { useDebounce } from 'use-debounce';
+import { getPhotosAction, searchPhotosPublicAction } from './actions';
+
+const formatQuery = (query: string) =>
+ query.trim().toLocaleLowerCase();
+
+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(() =>
+ formatQuery(query), [query]);
+ const [_queryDebounced] = useDebounce(query, 500, { leading: true });
+ const queryDebounced = useMemo(() =>
+ formatQuery(_queryDebounced), [_queryDebounced]);
+
+ const [photos, setPhotos] = useState([]);
+
+ const resultsNotFound =
+ queryDebounced.length >= minimumQueryLength &&
+ !isLoading &&
+ photos.length === 0;
+
+ const reset = useCallback(() => {
+ setPhotos([]);
+ setIsLoading(false);
+ }, []);
+
+ useEffect(() => {
+ if (queryDebounced.length >= minimumQueryLength && isEnabled) {
+ setIsLoading(true);
+ (isPrivate
+ ? getPhotosAction({ query: queryDebounced })
+ : searchPhotosPublicAction(queryDebounced))
+ .then(setPhotos)
+ .finally(() => setIsLoading(false));
+ }
+ }, [
+ queryDebounced,
+ minimumQueryLength,
+ isEnabled,
+ isPrivate,
+ ]);
+
+ useEffect(() => {
+ if (queryFormatted.length >= minimumQueryLength) {
+ setIsLoading(true);
+ } else {
+ setPhotos([]);
+ setIsLoading(false);
+ }
+ }, [minimumQueryLength, queryFormatted]);
+
+ return {
+ queryFormatted,
+ photos,
+ isLoading,
+ resultsNotFound,
+ reset,
+ };
+}