diff --git a/src/components/CommandKClient.tsx b/src/components/CommandKClient.tsx index 3cfe3cfa..67e0c64c 100644 --- a/src/components/CommandKClient.tsx +++ b/src/components/CommandKClient.tsx @@ -8,6 +8,7 @@ import { useDebounce } from 'use-debounce'; import Spinner from './Spinner'; import { useRouter } from 'next/navigation'; import { useTheme } from 'next-themes'; +import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi'; const LISTENER_KEYDOWN = 'keydown'; @@ -16,7 +17,8 @@ export type CommandKSection = { accessory?: ReactNode items: { label: string - note?: string + annotation?: ReactNode + annotationAria?: string accessory?: ReactNode path?: string action?: () => void @@ -32,7 +34,7 @@ export default function CommandKClient({ }) { const [isOpen, setIsOpen] = useState(false); const [queryRaw, setQueryRaw] = useState(''); - const [queryDebounced] = useDebounce(queryRaw, 500); + const [queryDebounced] = useDebounce(queryRaw, 500, { trailing: true }); const [isLoading, setIsLoading] = useState(false); const [queriedSections, setQueriedSections] = useState([]); @@ -70,16 +72,27 @@ export default function CommandKClient({ } }, [queryRaw]); + useEffect(() => { + if (!isOpen) { + setQueryRaw(''); + setQueriedSections([]); + setIsLoading(false); + } + }, [isOpen]); + const sectionTheme: CommandKSection = { heading: 'Theme', items: [{ label: 'Use System', + annotation: , action: () => setTheme('system'), }, { label: 'Light Mode', + annotation: , action: () => setTheme('light'), }, { label: 'Dark Mode', + annotation: , action: () => setTheme('dark'), }], }; @@ -87,13 +100,7 @@ export default function CommandKClient({ return ( { - if (!isOpen) { - setQueryRaw(''); - setQueriedSections([]); - } - setIsOpen(isOpen); - }} + onOpenChange={setIsOpen} label="Global Command Menu" filter={(value, search) => value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0} @@ -132,10 +139,17 @@ export default function CommandKClient({ .concat(sections) .concat(sectionTheme) .filter(({ items }) => items.length > 0) - .map(({ heading, items }) => + .map(({ heading, accessory, items }) => + {accessory && +
{accessory}
} + {heading} + } className={clsx( 'uppercase', 'select-none', @@ -146,7 +160,14 @@ export default function CommandKClient({ '[&>*:first-child]:tracking-wider', )} > - {items.map(({ accessory, label, note, path, action }) => + {items.map(({ + accessory, + label, + annotation, + annotationAria, + path, + action, + }) => {label} - {note && - - {note} + {annotation && + + + {annotation} + } )} diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 4d98bc71..3c33b47e 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -24,6 +24,7 @@ import { getDimensionsFromSize } from '@/utility/size'; import ImageBlurFallback from '@/components/ImageBlurFallback'; import { BLUR_ENABLED } from '@/site/config'; import { Tags, sortTagsObjectWithoutFavs } from '@/tag'; +import { formatCount, formatCountDescriptive } from '@/utility/string'; const THUMBNAIL_SIZE = 300; @@ -150,9 +151,8 @@ export default function PhotoForm({ sortTagsObjectWithoutFavs(uniqueTags ?? []) .map(({ tag, count }) => ({ value: tag, - annotation: `× ${count}`, - annotationAria: - `tagged in ${count} photo${count === 1 ? '' : 's'}`, + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count, 'tagged'), })) ) .map(([key, { diff --git a/src/site/CommandK.tsx b/src/site/CommandK.tsx index 02383b1d..7ff813ff 100644 --- a/src/site/CommandK.tsx +++ b/src/site/CommandK.tsx @@ -20,6 +20,13 @@ import { getPhotos } from '@/services/vercel-postgres'; import { titleForPhoto } from '@/photo'; import PhotoTiny from '@/photo/PhotoTiny'; import { formatDate } from '@/utility/date'; +import { formatCount, formatCountDescriptive } from '@/utility/string'; +import { BiLockAlt } from 'react-icons/bi'; +import { sortTagsObject } from '@/tag'; +import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon'; +import { IoMdCamera } from 'react-icons/io'; +import { FaTag } from 'react-icons/fa'; +import { TbPhoto } from 'react-icons/tb'; export default async function CommandK() { const [ @@ -38,47 +45,65 @@ export default async function CommandK() { const SECTION_TAGS: CommandKSection = { heading: 'Tags', - items: tags.map(({ tag }) => ({ + accessory: , + items: sortTagsObject(tags).map(({ tag, count }) => ({ label: tag, + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), path: pathForTag(tag), })), }; const SECTION_CAMERAS: CommandKSection = { heading: 'Cameras', - items: cameras.map(({ camera }) => ({ + accessory: , + items: cameras.map(({ camera, count }) => ({ label: formatCameraText(camera), + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), path: pathForCamera(camera), })), }; const SECTION_FILM: CommandKSection = { heading: 'Film Simulations', - items: filmSimulations.map(({ simulation }) => ({ + accessory: + + , + items: filmSimulations.map(({ simulation, count }) => ({ label: simulation, + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), path: pathForFilmSimulation(simulation), })), }; const SECTION_PAGES: CommandKSection = { heading: 'Pages', - items: [{ + items: ([{ label: 'Home', path: '/', }, { label: 'Grid', path:'/grid', - }].concat(showAdminPages ? [{ - label: 'Admin » Photos', + }] as CommandKSection['items']).concat(showAdminPages ? [{ + label: 'Admin / Photos', + annotation: , path: PATH_ADMIN_PHOTOS, }, { - label: 'Admin » Uploads', + label: 'Admin / Uploads', + annotation: , path: PATH_ADMIN_UPLOADS, }, { - label: 'Admin » Tags', + label: 'Admin / Tags', + annotation: , path: PATH_ADMIN_TAGS, }, { - label: 'Admin » Config', + label: 'Admin / Config', + annotation: , path: PATH_ADMIN_CONFIGURATION, }] : []), }; @@ -97,10 +122,11 @@ export default async function CommandK() { return photos.length > 0 ? [{ heading: 'Photos', + accessory: , items: photos.map(photo => ({ accessory: , label: titleForPhoto(photo), - note: formatDate(photo.takenAt), + annotation: formatDate(photo.takenAt), path: pathForPhoto(photo), })), }] diff --git a/src/utility/string.ts b/src/utility/string.ts index c9718b4a..64bb94bd 100644 --- a/src/utility/string.ts +++ b/src/utility/string.ts @@ -24,3 +24,14 @@ export const parameterize = (string: string) => // Removes all non-alphanumeric characters .replaceAll(/([^a-z0-9-])/gi, '') .toLowerCase(); + +export const formatCount = (count: number) => `× ${count}`; + +export const formatCountDescriptive = ( + count: number, + verb = 'found', + noun = 'photo', + singular = '', + plural = 's', +) => + `${verb} in ${count} ${noun}${count === 1 ? singular : plural}`;