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 { updateAboutAction } from './actions';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import { Photo } from '@/photo'; 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 { useAppText } from '@/i18n/state/client';
import FieldsetPhotoChooser from '@/photo/form/FieldsetPhotoChooser'; import FieldsetPhotoChooser from '@/photo/form/FieldsetPhotoChooser';
export default function AdminAboutEditPage({ export default function AdminAboutEditPage({
about, about,
photoAvatar: _photoAvatar, photoAvatar,
photoHero: _photoHero, photoHero,
photos, photos,
photosCount, photosCount,
photosFavs, photosFavs,
@ -36,22 +32,6 @@ export default function AdminAboutEditPage({
const [aboutForm, setAboutForm] = useState<Partial<AboutInsert>>(about ?? {}); 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(); const convertUrlToPhotoId = (url?: string) => url?.split('/').pop();
return ( return (
@ -66,7 +46,8 @@ export default function AdminAboutEditPage({
> >
<div className="space-y-4"> <div className="space-y-4">
<FieldsetPhotoChooser <FieldsetPhotoChooser
label="Avatar Photo" id="photoIdAvatar"
label="Avatar"
value={aboutForm?.photoIdAvatar ?? ''} value={aboutForm?.photoIdAvatar ?? ''}
onChange={photoIdAvatar => setAboutForm(form => onChange={photoIdAvatar => setAboutForm(form =>
({ ...form, photoIdAvatar }))} ({ ...form, photoIdAvatar }))}
@ -75,16 +56,6 @@ export default function AdminAboutEditPage({
photosCount={photosCount} photosCount={photosCount}
photosFavs={photosFavs} 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 <FieldsetWithStatus
label="Title" label="Title"
value={aboutForm?.title ?? ''} value={aboutForm?.title ?? ''}
@ -106,22 +77,17 @@ export default function AdminAboutEditPage({
onChange={description => setAboutForm(form => onChange={description => setAboutForm(form =>
({ ...form, description }))} ({ ...form, description }))}
/> />
<FieldsetWithStatus <FieldsetPhotoChooser
id="photoIdHero" id="photoIdHero"
label="Hero Photo Id" label="Hero"
spellCheck={false}
value={aboutForm?.photoIdHero ?? ''} value={aboutForm?.photoIdHero ?? ''}
onChange={photoIdHero => setAboutForm(form => onChange={photoIdHero => setAboutForm(form =>
({ ...form, photoIdHero: convertUrlToPhotoId(photoIdHero) }))} ({ ...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>
<div className="flex gap-2"> <div className="flex gap-2">
<LinkWithStatus <LinkWithStatus

View File

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