Add header icons/annotations to cmd-k menu, optimize behavior

This commit is contained in:
Sam Becker 2024-02-20 17:13:39 -06:00
parent 6221773cf9
commit 6aa351cf29
4 changed files with 91 additions and 28 deletions

View File

@ -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<CommandKSection[]>([]);
@ -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: <BiDesktop />,
action: () => setTheme('system'),
}, {
label: 'Light Mode',
annotation: <BiSun size={16} className="translate-x-[1.25px]" />,
action: () => setTheme('light'),
}, {
label: 'Dark Mode',
annotation: <BiMoon className="translate-x-[1px]" />,
action: () => setTheme('dark'),
}],
};
@ -87,13 +100,7 @@ export default function CommandKClient({
return (
<Command.Dialog
open={isOpen}
onOpenChange={isOpen => {
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 }) =>
<Command.Group
key={heading}
heading={heading}
heading={<div className={clsx(
'flex items-center',
'px-2',
)}>
{accessory &&
<div className="w-5">{accessory}</div>}
{heading}
</div>}
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,
}) =>
<Command.Item
key={`${heading}-${label}`}
value={`${heading}-${label}`}
@ -170,9 +191,14 @@ export default function CommandKClient({
<span className="grow text-ellipsis truncate">
{label}
</span>
{note &&
<span className="text-dim whitespace-nowrap">
{note}
{annotation &&
<span
className="text-dim whitespace-nowrap"
aria-label={annotationAria}
>
<span aria-hidden={Boolean(annotationAria)}>
{annotation}
</span>
</span>}
</div>
</Command.Item>)}

View File

@ -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, {

View File

@ -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: <FaTag
size={10}
className="translate-x-[1px] translate-y-[0.5px]"
/>,
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: <IoMdCamera />,
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: <span className="w-3">
<PhotoFilmSimulationIcon />
</span>,
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: <BiLockAlt />,
path: PATH_ADMIN_PHOTOS,
}, {
label: 'Admin » Uploads',
label: 'Admin / Uploads',
annotation: <BiLockAlt />,
path: PATH_ADMIN_UPLOADS,
}, {
label: 'Admin » Tags',
label: 'Admin / Tags',
annotation: <BiLockAlt />,
path: PATH_ADMIN_TAGS,
}, {
label: 'Admin » Config',
label: 'Admin / Config',
annotation: <BiLockAlt />,
path: PATH_ADMIN_CONFIGURATION,
}] : []),
};
@ -97,10 +122,11 @@ export default async function CommandK() {
return photos.length > 0
? [{
heading: 'Photos',
accessory: <TbPhoto size={14} />,
items: photos.map(photo => ({
accessory: <PhotoTiny photo={photo} />,
label: titleForPhoto(photo),
note: formatDate(photo.takenAt),
annotation: formatDate(photo.takenAt),
path: pathForPhoto(photo),
})),
}]

View File

@ -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}`;