Add photo chooser empty states

This commit is contained in:
Sam Becker 2026-03-01 19:56:07 -06:00
parent f244b8ce94
commit 8a6131d539
4 changed files with 68 additions and 30 deletions

View File

@ -11,7 +11,7 @@ import { FiMoreHorizontal } from 'react-icons/fi';
import MoreMenuItem from './MoreMenuItem';
import { clearGlobalFocus } from '@/utility/dom';
import { FaChevronRight } from 'react-icons/fa6';
import { menuSurfaceStyles } from '../primitives/surface';
import { MENU_SURFACE_STYLES } from '../primitives/surface';
export type MoreMenuSection = {
label?: string
@ -101,7 +101,10 @@ export default function MoreMenu({
onCloseAutoFocus={e => e.preventDefault()}
align={align}
sideOffset={sideOffset}
className={menuSurfaceStyles(className)}
className={clsx(
MENU_SURFACE_STYLES,
className,
)}
>
{header && <div className={clsx(
'px-3 pt-3 pb-2 text-dim uppercase',
@ -157,7 +160,7 @@ export default function MoreMenu({
</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent
className={menuSurfaceStyles()}
className={MENU_SURFACE_STYLES}
>
{item.items.map(item =>
<div key={item.label} className="px-1">

View File

@ -1,6 +1,6 @@
import clsx from 'clsx/lite';
export const menuSurfaceStyles = (className?: string) => clsx(
export const MENU_SURFACE_STYLES = clsx(
'z-10',
'min-w-[8rem]',
'component-surface',
@ -12,5 +12,4 @@ export const menuSurfaceStyles = (className?: string) => clsx(
'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,
);

View File

@ -9,17 +9,16 @@ import {
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 { 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 { BiChevronDown } from 'react-icons/bi';
import { BiChevronRight } from 'react-icons/bi';
import SegmentMenu from '@/components/SegmentMenu';
import IconFavs from '@/components/icons/IconFavs';
import InfinitePhotoScroll from '../InfinitePhotoScroll';
// TODO:
// Create empty state for all modes, including no search results
import AdminEmptyState from '@/admin/AdminEmptyState';
import { TbPhotoSearch } from 'react-icons/tb';
type Mode = 'all' | 'favs' | 'search';
@ -66,6 +65,7 @@ export default function FieldsetPhotoChooser({
photos: photosQuery,
isLoading: isLoadingPhotoQuery,
reset: resetPhotoQuery,
resultsNotFound,
} = usePhotoQuery({ query, isPrivate: true });
const reset = useCallback((resetMenu?: boolean) => {
@ -125,11 +125,18 @@ export default function FieldsetPhotoChooser({
'font-sans',
'text-xs text-medium font-medium uppercase tracking-wider',
'py-1',
'select-none',
)}>
<span className="grow truncate text-left">
{label}
</span>
<BiChevronDown size={18} />
<BiChevronRight
size={18}
className={clsx(
'transition-transform ',
isOpen && 'rotate-90',
)}
/>
</span>
<span className={clsx(
'flex size-[6rem]',
@ -144,8 +151,11 @@ export default function FieldsetPhotoChooser({
<DropdownMenu.Content
onCloseAutoFocus={e => e.preventDefault()}
align="start"
sideOffset={10}
className={menuSurfaceStyles('z-20 rounded-2xl pb-0 overflow-auto')}
sideOffset={-80}
className={clsx(
MENU_SURFACE_STYLES,
'z-20 rounded-2xl pb-0 overflow-auto',
)}
>
<SegmentMenu
className="pt-1 pb-2 px-1.5"
@ -168,26 +178,46 @@ export default function FieldsetPhotoChooser({
}
}}
/>
<div className={clsx(
'w-[18rem] h-[20rem] overflow-y-auto',
'space-y-0.5',
)}>
<div className={clsx(
'flex items-center transition-all overflow-hidden',
showQuery ? 'h-12 opacity-100' : 'h-0 opacity-0',
)}>
<div className="p-1 border-t border-medium w-full">
<div className="w-full px-1.5">
<input
id="query"
ref={inputRef}
type="text"
placeholder="Search for a photo"
className="block w-full m-0 border-none outline-none"
className={clsx(
'block w-full m-0 outline-none',
'rounded-full border border-dim',
'mb-2',
)}
value={query}
onChange={e => setQuery(e.target.value)}
/>
</div>
</div>
<div className={clsx(
'w-[18rem] max-h-[20rem] overflow-y-auto',
'space-y-0.5',
)}>
{showQuery && resultsNotFound &&
<AdminEmptyState
icon={<IoSearch className="text-dim" />}
className="translate-y-8"
includeContainer={false}
>
No photos found
</AdminEmptyState>}
{!showQuery && photosToShow.length === 0 &&
<AdminEmptyState
icon={<TbPhotoSearch className="text-dim" />}
className="translate-y-16"
includeContainer={false}
>
No photos
</AdminEmptyState>}
<div className={CLASSNAME_GRID}>
{photosToShow.map(photo => renderPhotoButton(photo))}
</div>

View File

@ -28,6 +28,11 @@ export default function usePhotoQuery({
const [photos, setPhotos] = useState<Photo[]>([]);
const resultsNotFound =
queryDebounced.length >= minimumQueryLength &&
!isLoading &&
photos.length === 0;
const reset = useCallback(() => {
setPhotos([]);
setIsLoading(false);
@ -62,6 +67,7 @@ export default function usePhotoQuery({
queryFormatted,
photos,
isLoading,
resultsNotFound,
reset,
};
}