Refine photo chooser behavior

This commit is contained in:
Sam Becker 2026-03-01 20:48:32 -06:00
parent 669d471dc0
commit af6f75fa0b
2 changed files with 45 additions and 67 deletions

View File

@ -9,17 +9,13 @@ import AdminChildPage from '@/components/AdminChildPage';
import { updateAboutAction } from './actions';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import { Photo } from '@/photo';
import PhotoAvatar from '@/photo/PhotoAvatar';
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,
photoAvatar,
photoHero,
photos,
photosCount,
photosFavs,
@ -36,22 +32,6 @@ export default function AdminAboutEditPage({
const [aboutForm, setAboutForm] = useState<Partial<AboutInsert>>(about ?? {});
const {
photo: photoAvatar,
isLoading: isLoadingPhotoAvatar,
} = useDynamicPhoto({
initialPhoto: _photoAvatar,
photoId: aboutForm?.photoIdAvatar,
});
const {
photo: photoHero,
isLoading: isLoadingPhotoHero,
} = useDynamicPhoto({
initialPhoto: _photoHero,
photoId: aboutForm?.photoIdHero,
});
const convertUrlToPhotoId = (url?: string) => url?.split('/').pop();
return (
@ -66,7 +46,8 @@ export default function AdminAboutEditPage({
>
<div className="space-y-4">
<FieldsetPhotoChooser
label="Avatar Photo"
id="photoIdAvatar"
label="Avatar"
value={aboutForm?.photoIdAvatar ?? ''}
onChange={photoIdAvatar => setAboutForm(form =>
({ ...form, photoIdAvatar }))}
@ -75,16 +56,6 @@ export default function AdminAboutEditPage({
photosCount={photosCount}
photosFavs={photosFavs}
/>
<PhotoAvatar photo={photoAvatar} />
<FieldsetWithStatus
id="photoIdAvatar"
label="Avatar Photo Id"
spellCheck={false}
value={aboutForm?.photoIdAvatar ?? ''}
onChange={photoIdAvatar => setAboutForm(form =>
({ ...form, photoIdAvatar: convertUrlToPhotoId(photoIdAvatar) }))}
loading={isLoadingPhotoAvatar}
/>
<FieldsetWithStatus
label="Title"
value={aboutForm?.title ?? ''}
@ -106,22 +77,17 @@ export default function AdminAboutEditPage({
onChange={description => setAboutForm(form =>
({ ...form, description }))}
/>
<FieldsetWithStatus
<FieldsetPhotoChooser
id="photoIdHero"
label="Hero Photo Id"
spellCheck={false}
label="Hero"
value={aboutForm?.photoIdHero ?? ''}
onChange={photoIdHero => setAboutForm(form =>
({ ...form, photoIdHero: convertUrlToPhotoId(photoIdHero) }))}
loading={isLoadingPhotoHero}
photo={photoHero}
photos={photos}
photosCount={photosCount}
photosFavs={photosFavs}
/>
{photoHero &&
<div className={clsx(
'w-24 overflow-hidden rounded-md',
'border border-medium bg-dim',
)}>
<PhotoMedium photo={photoHero} />
</div>}
</div>
<div className="flex gap-2">
<LinkWithStatus

View File

@ -11,7 +11,13 @@ 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 {
ComponentProps,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import usePhotoQuery from '../usePhotoQuery';
import { BiChevronRight } from 'react-icons/bi';
import SegmentMenu from '@/components/SegmentMenu';
@ -19,6 +25,7 @@ import IconFavs from '@/components/icons/IconFavs';
import InfinitePhotoScroll from '../InfinitePhotoScroll';
import AdminEmptyState from '@/admin/AdminEmptyState';
import { TbPhotoSearch } from 'react-icons/tb';
import { MdOutlineNoPhotography } from 'react-icons/md';
type Mode = 'all' | 'favs' | 'search';
@ -34,22 +41,17 @@ const renderPhoto = (photo: Photo) =>
/>;
export default function FieldsetPhotoChooser({
label,
value,
onChange,
photo: _photo,
photos = [],
photosCount,
photosFavs,
...props
}: {
label: string
value: string
onChange: (photoId: string) => void
photo?: Photo
photos: Photo[]
photosCount: number
photosFavs: Photo[]
}) {
} & ComponentProps<typeof FieldsetWithStatus>) {
const [isOpen, setIsOpen] = useState(false);
const [photo, setPhoto] = useState(_photo);
@ -58,7 +60,8 @@ export default function FieldsetPhotoChooser({
const showQuery = mode === 'search';
const inputRef = useRef<HTMLInputElement>(null);
const refContainer = useRef<HTMLDivElement>(null);
const refInput = useRef<HTMLInputElement>(null);
const [query, setQuery] = useState('');
const {
@ -74,11 +77,6 @@ export default function FieldsetPhotoChooser({
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); }
@ -94,7 +92,7 @@ export default function FieldsetPhotoChooser({
)}
onClick={() => {
setPhoto(photo);
onChange(photo.id);
props.onChange?.(photo.id);
setIsOpen(false);
}}
>
@ -113,7 +111,7 @@ export default function FieldsetPhotoChooser({
return (
<>
<FieldsetWithStatus {...{ label, value, onChange, type: 'hidden' }} />
<FieldsetWithStatus {...props} type="hidden" />
<DropdownMenu.Root open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenu.Trigger asChild>
<button type="button" className={clsx(
@ -128,7 +126,7 @@ export default function FieldsetPhotoChooser({
'select-none',
)}>
<span className="grow truncate text-left">
{label}
{props.label}
</span>
<BiChevronRight
size={18}
@ -143,8 +141,16 @@ export default function FieldsetPhotoChooser({
'flex size-[6rem]',
'border border-medium rounded-[4px]',
'overflow-hidden select-none active:opacity-75',
'bg-extra-dim',
)}>
{photo && renderPhoto(photo)}
{photo
? renderPhoto(photo)
: <div className="flex items-center justify-center w-full">
<MdOutlineNoPhotography
size={24}
className="text-dim"
/>
</div>}
</span>
</button>
</DropdownMenu.Trigger>
@ -176,13 +182,19 @@ export default function FieldsetPhotoChooser({
setMode(mode);
if (mode !== 'search') {
reset();
} else {
refContainer.current?.scrollTo({ top: 0 });
refInput.current?.focus();
}
}}
/>
<div className={clsx(
'w-[18rem] h-[20rem] overflow-y-auto',
'space-y-0.5',
)}>
<div
ref={refContainer}
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',
@ -190,7 +202,7 @@ export default function FieldsetPhotoChooser({
<div className="w-full px-1.5">
<input
id="query"
ref={inputRef}
ref={refInput}
type="text"
placeholder="Search for a photo"
className={clsx(