'use client'; import { Command } from 'cmdk'; import { ReactNode, useEffect, useState } from 'react'; import Modal from './Modal'; import { clsx } from 'clsx/lite'; 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'; import { IoInvertModeSharp } from 'react-icons/io5'; const LISTENER_KEYDOWN = 'keydown'; const MINIMUM_QUERY_LENGTH = 2; export type CommandKSection = { heading: string accessory?: ReactNode items: { label: string annotation?: ReactNode annotationAria?: string accessory?: ReactNode path?: string action?: () => void }[] } export default function CommandKClient({ onQueryChange, sections = [], }: { onQueryChange?: (query: string) => Promise sections?: CommandKSection[] }) { const [isOpen, setIsOpen] = useState(false); const [queryRaw, setQueryRaw] = useState(''); const [queryDebounced] = useDebounce(queryRaw, 500, { trailing: true }); const [isLoading, setIsLoading] = useState(false); const [queriedSections, setQueriedSections] = useState([]); const { setTheme } = useTheme(); const router = useRouter(); useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setIsOpen((open) => !open); } }; document.addEventListener(LISTENER_KEYDOWN, down); return () => document.removeEventListener(LISTENER_KEYDOWN, down); }, []); useEffect(() => { if (queryDebounced.length >= MINIMUM_QUERY_LENGTH) { setIsLoading(true); onQueryChange?.(queryDebounced).then(querySections => { setQueriedSections(querySections); setIsLoading(false); }); } }, [queryDebounced, onQueryChange]); useEffect(() => { if (queryRaw === '') { setQueriedSections([]); } else if (queryRaw.length >= MINIMUM_QUERY_LENGTH) { setIsLoading(true); } }, [queryRaw]); useEffect(() => { if (!isOpen) { setQueryRaw(''); setQueriedSections([]); setIsLoading(false); } }, [isOpen]); const sectionTheme: CommandKSection = { heading: 'Theme', accessory: , items: [{ label: 'Use System', annotation: , action: () => setTheme('system'), }, { label: 'Light Mode', annotation: , action: () => setTheme('light'), }, { label: 'Dark Mode', annotation: , action: () => setTheme('dark'), }], }; return ( value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0} loop > setIsOpen(false)} fast >
setQueryRaw(e.currentTarget.value)} className={clsx( 'w-full', 'focus:ring-0', '!border-gray-200 dark:!border-gray-800', 'focus:border-gray-200 focus:dark:border-gray-800', 'placeholder:text-gray-400/80', 'placeholder:dark:text-gray-700', )} style={{ paddingRight: '2rem' }} placeholder="Search photos, views, settings ..." /> {isLoading && }
{isLoading ? 'Searching ...' : 'No results found'} {queriedSections .concat(sections) .concat(sectionTheme) .filter(({ items }) => items.length > 0) .map(({ heading, accessory, items }) => {accessory &&
{accessory}
} {heading}
} className={clsx( 'uppercase', 'select-none', '[&>*:first-child]:py-1', '[&>*:first-child]:font-medium', '[&>*:first-child]:text-dim', '[&>*:first-child]:text-xs', '[&>*:first-child]:tracking-wider', )} > {items.map(({ accessory, label, annotation, annotationAria, path, action, }) => { setIsOpen(false); action?.(); if (path) { router.push(path); } }} >
{accessory} {label} {annotation && {annotation} }
)} )}
); }