Extract photo query logic to hook
This commit is contained in:
parent
0465b51427
commit
7baedd0700
@ -2,7 +2,7 @@ import AdminComponentPageClient from '@/admin/AdminComponentPageClient';
|
|||||||
import { getPhotosCached, getPhotosMetaCached } from '@/photo/cache';
|
import { getPhotosCached, getPhotosMetaCached } from '@/photo/cache';
|
||||||
|
|
||||||
export default async function ComponentsPage() {
|
export default async function ComponentsPage() {
|
||||||
const photos = await getPhotosCached();
|
const photos = await getPhotosCached({ limit: 50 });
|
||||||
const photosCount = await getPhotosMetaCached()
|
const photosCount = await getPhotosMetaCached()
|
||||||
.then(({ count }) => count);
|
.then(({ count }) => count);
|
||||||
|
|
||||||
|
|||||||
@ -37,14 +37,12 @@ import {
|
|||||||
} from '../app/path';
|
} from '../app/path';
|
||||||
import Modal from '../components/Modal';
|
import Modal from '../components/Modal';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { useDebounce } from 'use-debounce';
|
|
||||||
import Spinner from '../components/Spinner';
|
import Spinner from '../components/Spinner';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { BiDesktop, BiLockAlt, BiMoon, BiSun } from 'react-icons/bi';
|
import { BiDesktop, BiLockAlt, BiMoon, BiSun } from 'react-icons/bi';
|
||||||
import { IoClose, IoInvertModeSharp } from 'react-icons/io5';
|
import { IoClose, IoInvertModeSharp } from 'react-icons/io5';
|
||||||
import { useAppState } from '@/app/AppState';
|
import { useAppState } from '@/app/AppState';
|
||||||
import { searchPhotosAction } from '@/photo/actions';
|
|
||||||
import { RiToolsFill } from 'react-icons/ri';
|
import { RiToolsFill } from 'react-icons/ri';
|
||||||
import { signOutAction } from '@/auth/actions';
|
import { signOutAction } from '@/auth/actions';
|
||||||
import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
|
import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
|
||||||
@ -98,12 +96,12 @@ import { getSortStateFromPath } from '@/photo/sort/path';
|
|||||||
import IconSort from '@/components/icons/IconSort';
|
import IconSort from '@/components/icons/IconSort';
|
||||||
import { useSelectPhotosState } from '@/admin/select/SelectPhotosState';
|
import { useSelectPhotosState } from '@/admin/select/SelectPhotosState';
|
||||||
import IconAlbum from '@/components/icons/IconAlbum';
|
import IconAlbum from '@/components/icons/IconAlbum';
|
||||||
|
import usePhotoQuery from '@/photo/usePhotoQuery';
|
||||||
|
|
||||||
const DIALOG_TITLE = 'Global Command-K Menu';
|
const DIALOG_TITLE = 'Global Command-K Menu';
|
||||||
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
|
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
|
||||||
|
|
||||||
const LISTENER_KEYDOWN = 'keydown';
|
const LISTENER_KEYDOWN = 'keydown';
|
||||||
const MINIMUM_QUERY_LENGTH = 2;
|
|
||||||
|
|
||||||
const MAX_HEIGHT = '20rem';
|
const MAX_HEIGHT = '20rem';
|
||||||
|
|
||||||
@ -246,19 +244,13 @@ export default function CommandKClient({
|
|||||||
}
|
}
|
||||||
}, [isWaiting, setIsOpen]);
|
}, [isWaiting, setIsOpen]);
|
||||||
|
|
||||||
// Raw query values
|
const [query, setQuery] = useState('');
|
||||||
const [queryLiveRaw, setQueryLiveRaw] = useState('');
|
const {
|
||||||
const [queryDebouncedRaw] =
|
queryFormatted,
|
||||||
useDebounce(queryLiveRaw, 500, { trailing: true });
|
photos,
|
||||||
|
isLoading,
|
||||||
// Parameterized query values
|
reset,
|
||||||
const queryLive = useMemo(() =>
|
} = usePhotoQuery(query, !isPending);
|
||||||
queryLiveRaw.trim().toLocaleLowerCase(), [queryLiveRaw]);
|
|
||||||
const queryDebounced = useMemo(() =>
|
|
||||||
queryDebouncedRaw.trim().toLocaleLowerCase(), [queryDebouncedRaw]);
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [queriedSections, setQueriedSections] = useState<CommandKSection[]>([]);
|
|
||||||
|
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
@ -283,55 +275,32 @@ export default function CommandKClient({
|
|||||||
return () => document.removeEventListener(LISTENER_KEYDOWN, down);
|
return () => document.removeEventListener(LISTENER_KEYDOWN, down);
|
||||||
}, [setIsOpen]);
|
}, [setIsOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
const queriedSections = useMemo<CommandKSection[]>(() => {
|
||||||
if (queryDebounced.length >= MINIMUM_QUERY_LENGTH && !isPending) {
|
if (isOpenRef.current && photos.length > 0) {
|
||||||
setIsLoading(true);
|
return [{
|
||||||
searchPhotosAction(queryDebounced)
|
heading: 'Photos',
|
||||||
.then(photos => {
|
accessory: <IconPhoto size={14} />,
|
||||||
if (isOpenRef.current) {
|
items: photos.map(photo => ({
|
||||||
setQueriedSections(photos.length > 0
|
label: titleForPhoto(photo),
|
||||||
? [{
|
keywords: getKeywordsForPhoto(photo),
|
||||||
heading: 'Photos',
|
annotation: <PhotoDate {...{ photo, timezone: undefined }} />,
|
||||||
accessory: <IconPhoto size={14} />,
|
accessory: <PhotoSmall photo={photo} />,
|
||||||
items: photos.map(photo => ({
|
path: pathForPhoto({ photo }),
|
||||||
label: titleForPhoto(photo),
|
})),
|
||||||
keywords: getKeywordsForPhoto(photo),
|
}];
|
||||||
annotation: <PhotoDate {...{ photo, timezone: undefined }} />,
|
} else {
|
||||||
accessory: <PhotoSmall photo={photo} />,
|
return [];
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [queryDebounced, isPending, appText]);
|
},
|
||||||
|
[photos],
|
||||||
useEffect(() => {
|
);
|
||||||
if (queryLive === '') {
|
|
||||||
setQueriedSections([]);
|
|
||||||
setIsLoading(false);
|
|
||||||
} else if (queryLive.length >= MINIMUM_QUERY_LENGTH) {
|
|
||||||
setIsLoading(true);
|
|
||||||
}
|
|
||||||
}, [queryLive]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setQueryLiveRaw('');
|
setQuery('');
|
||||||
setQueriedSections([]);
|
reset();
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen, reset]);
|
||||||
|
|
||||||
const recent = recents[0];
|
const recent = recents[0];
|
||||||
const recentsStatus = useMemo(() => {
|
const recentsStatus = useMemo(() => {
|
||||||
@ -345,17 +314,17 @@ export default function CommandKClient({
|
|||||||
|
|
||||||
// Years only accessible by search
|
// Years only accessible by search
|
||||||
const years = useMemo(() =>
|
const years = useMemo(() =>
|
||||||
_years.filter(({ year }) => queryLive && year.includes(queryLive))
|
_years.filter(({ year }) => queryFormatted && year.includes(queryFormatted))
|
||||||
, [_years, queryLive]);
|
, [_years, queryFormatted]);
|
||||||
|
|
||||||
const tags = useMemo(() => {
|
const tags = useMemo(() => {
|
||||||
const tagsIncludingPrivate = photosCountHidden > 0
|
const tagsIncludingPrivate = photosCountHidden > 0
|
||||||
? addPrivateToTags(_tags, photosCountHidden)
|
? addPrivateToTags(_tags, photosCountHidden)
|
||||||
: _tags;
|
: _tags;
|
||||||
return HIDE_TAGS_WITH_ONE_PHOTO
|
return HIDE_TAGS_WITH_ONE_PHOTO
|
||||||
? limitTagsByCount(tagsIncludingPrivate, 2, queryLive)
|
? limitTagsByCount(tagsIncludingPrivate, 2, queryFormatted)
|
||||||
: tagsIncludingPrivate;
|
: tagsIncludingPrivate;
|
||||||
}, [_tags, photosCountHidden, queryLive]);
|
}, [_tags, photosCountHidden, queryFormatted]);
|
||||||
|
|
||||||
const categorySections: CommandKSection[] = useMemo(() =>
|
const categorySections: CommandKSection[] = useMemo(() =>
|
||||||
CATEGORY_VISIBILITY
|
CATEGORY_VISIBILITY
|
||||||
@ -751,9 +720,9 @@ export default function CommandKClient({
|
|||||||
)}>
|
)}>
|
||||||
<Command.Input
|
<Command.Input
|
||||||
ref={refInput}
|
ref={refInput}
|
||||||
value={queryLiveRaw}
|
value={query}
|
||||||
onValueChange={value => {
|
onValueChange={value => {
|
||||||
setQueryLiveRaw(value);
|
setQuery(value);
|
||||||
updateMask();
|
updateMask();
|
||||||
}}
|
}}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -782,15 +751,15 @@ export default function CommandKClient({
|
|||||||
'text-gray-400/90 dark:text-gray-700',
|
'text-gray-400/90 dark:text-gray-700',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (queryLiveRaw) {
|
if (query) {
|
||||||
setQueryLiveRaw('');
|
setQuery('');
|
||||||
updateMask();
|
updateMask();
|
||||||
} else {
|
} else {
|
||||||
setIsOpen?.(false);
|
setIsOpen?.(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{queryLiveRaw
|
{query
|
||||||
? <IoClose size={17} className="text-dim" />
|
? <IoClose size={17} className="text-dim" />
|
||||||
: <>
|
: <>
|
||||||
<span className="sm:hidden">
|
<span className="sm:hidden">
|
||||||
@ -889,7 +858,7 @@ export default function CommandKClient({
|
|||||||
/>;
|
/>;
|
||||||
})}
|
})}
|
||||||
</Command.Group>)}
|
</Command.Group>)}
|
||||||
{footer && !queryLive &&
|
{footer && !queryFormatted &&
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'text-center text-base text-dim pt-1',
|
'text-center text-base text-dim pt-1',
|
||||||
'pb-2',
|
'pb-2',
|
||||||
|
|||||||
58
src/photo/usePhotoQuery.ts
Normal file
58
src/photo/usePhotoQuery.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/* eslint-disable react-hooks/set-state-in-effect */
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Photo } from '.';
|
||||||
|
import { useDebounce } from 'use-debounce';
|
||||||
|
import { searchPhotosAction } from './actions';
|
||||||
|
|
||||||
|
const formatQuery = (query: string) =>
|
||||||
|
query.trim().toLocaleLowerCase();
|
||||||
|
|
||||||
|
export default function usePhotoQuery(
|
||||||
|
query: string,
|
||||||
|
isEnabled = true,
|
||||||
|
minimumQueryLength = 2,
|
||||||
|
) {
|
||||||
|
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<Photo[]>([]);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setPhotos([]);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (queryDebounced.length >= minimumQueryLength && isEnabled) {
|
||||||
|
setIsLoading(true);
|
||||||
|
searchPhotosAction(queryDebounced)
|
||||||
|
.then(setPhotos)
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
queryDebounced,
|
||||||
|
minimumQueryLength,
|
||||||
|
isEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (queryFormatted.length >= minimumQueryLength) {
|
||||||
|
setIsLoading(true);
|
||||||
|
} else {
|
||||||
|
setPhotos([]);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [minimumQueryLength, queryFormatted]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryFormatted,
|
||||||
|
photos,
|
||||||
|
isLoading,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user