Refine photo chooser behavior
This commit is contained in:
parent
669d471dc0
commit
af6f75fa0b
@ -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
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user