Seed basic command-k data

This commit is contained in:
Sam Becker 2024-02-19 12:23:33 -06:00
parent fdf53111cf
commit 1dd0ea9101
7 changed files with 193 additions and 57 deletions

View File

@ -48,6 +48,7 @@
"sonner": "^1.4.0",
"tailwindcss": "3.4.1",
"ts-exif-parser": "^0.2.2",
"typescript": "5.3.3"
"typescript": "5.3.3",
"use-debounce": "^10.0.0"
}
}

12
pnpm-lock.yaml generated
View File

@ -125,6 +125,9 @@ dependencies:
typescript:
specifier: 5.3.3
version: 5.3.3
use-debounce:
specifier: ^10.0.0
version: 10.0.0(react@18.2.0)
packages:
@ -7569,6 +7572,15 @@ packages:
tslib: 2.6.2
dev: false
/use-debounce@10.0.0(react@18.2.0):
resolution: {integrity: sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==}
engines: {node: '>= 16.0.0'}
peerDependencies:
react: '>=16.8.0'
dependencies:
react: 18.2.0
dev: false
/use-sidecar@1.1.2(@types/react@18.2.55)(react@18.2.0):
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}

View File

@ -13,7 +13,7 @@ import Footer from '@/site/Footer';
import { Suspense } from 'react';
import FooterClient from '@/site/FooterClient';
import NavClient from '@/site/NavClient';
import CommandK from '@/components/CommandK';
import CommandK from '@/site/CommandK';
import '../site/globals.css';

View File

@ -1,54 +0,0 @@
'use client';
import { Command } from 'cmdk';
import { useEffect, useState } from 'react';
import Modal from './Modal';
import { clsx } from 'clsx/lite';
const LISTENER_KEYDOWN = 'keydown';
export default function CommandK() {
const [open, setOpen] = useState(false);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener(LISTENER_KEYDOWN, down);
return () => document.removeEventListener(LISTENER_KEYDOWN, down);
}, []);
const renderItem = (item: string) =>
<Command.Item className={clsx(
'p-1 rounded-md',
'data-[selected=true]:bg-blue-50',
)}>
{item}
</Command.Item>;
return (
<Command.Dialog
open={open}
onOpenChange={setOpen}
label="Global Command Menu"
>
<Modal onClose={() => setOpen(false)} fast>
<Command.Input className="w-full" />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Letters">
{renderItem('a')}
{renderItem('b')}
<Command.Separator />
{renderItem('c')}
</Command.Group>
{renderItem('Apple')}
</Command.List>
</Modal>
</Command.Dialog>
);
}

View File

@ -0,0 +1,116 @@
'use client';
import { Command } from 'cmdk';
import { 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';
const LISTENER_KEYDOWN = 'keydown';
export default function CommandKClient({
isLoading,
onQueryChange,
sections = [],
}: {
isLoading?: boolean
onQueryChange?: (query: string) => void
sections?: {
heading: string
items: {
label: string
path: string
}[]
}[]
}) {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
const [queryDebounced] = useDebounce(query, 1000);
const router = useRouter();
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener(LISTENER_KEYDOWN, down);
return () => document.removeEventListener(LISTENER_KEYDOWN, down);
}, []);
useEffect(() => {
if (queryDebounced) {
onQueryChange?.(queryDebounced);
}
}, [queryDebounced, onQueryChange]);
return (
<Command.Dialog
open={open}
onOpenChange={setOpen}
label="Global Command Menu"
>
<Modal
anchor='top'
onClose={() => setOpen(false)}
fast
>
<div className="space-y-3">
<div className="text-sm uppercase text-dim">
Search for photos, tags, cameras, and film
</div>
<div className="relative">
<Command.Input
onChangeCapture={(e) => setQuery(e.currentTarget.value)}
className="w-full !min-w-0"
style={{ paddingRight: '2rem' }}
/>
{isLoading &&
<span className="absolute top-2.5 right-3">
<Spinner size={16} />
</span>}
</div>
<div
aria-hidden="true"
className={clsx(
'absolute bottom-4 inset-x-0 h-6 z-10 pointer-events-none',
'bg-gradient-to-t from-white to-transparent',
)}
/>
<Command.List className="relative max-h-72 pb-4 overflow-y-scroll">
<Command.Empty>No results found.</Command.Empty>
{sections
.filter(({ items }) => items.length > 0)
.map(({ heading, items }) =>
<Command.Group
key={heading}
heading={heading}
className="select-none"
>
{items.map(({ label, path }) =>
<Command.Item
key={label}
className={clsx(
'p-1 rounded-md cursor-pointer',
'data-[selected=true]:bg-gray-100',
'data-[selected=true]:dark:bg-gray-900/75',
'data-[active=true]:bg-green-400'
)}
onSelect={() => {
setOpen(false);
router.push(path);
}}
>
{label}
</Command.Item>)}
</Command.Group>)}
</Command.List>
</div>
</Modal>
</Command.Dialog>
);
}

View File

@ -12,11 +12,15 @@ import usePrefersReducedMotion from '@/utility/usePrefersReducedMotion';
export default function Modal({
onClosePath,
onClose,
className,
anchor = 'center',
children,
fast,
}: {
onClosePath?: string
onClose?: () => void
className?: string
anchor?: 'top' | 'center'
children: ReactNode
fast?: boolean
}) {
@ -51,7 +55,10 @@ export default function Modal({
return (
<motion.div
className={clsx(
'fixed inset-0 z-50 flex items-center justify-center',
'fixed inset-0 z-50 flex justify-center',
anchor === 'top'
? 'items-start pt-4 sm:pt-24'
: 'items-center',
'bg-black',
)}
initial={!prefersReducedMotion
@ -69,6 +76,7 @@ export default function Modal({
'bg-white dark:bg-black',
'dark:border dark:border-gray-800',
'md:p-4 md:rounded-xl',
className,
)}
style={{ width: 'min(500px, 90vw)' }}
ref={contentRef}

53
src/site/CommandK.tsx Normal file
View File

@ -0,0 +1,53 @@
import CommandKClient from '@/components/CommandKClient';
import {
getUniqueCamerasCached,
getUniqueFilmSimulationsCached,
getUniqueTagsCached,
} from '@/photo/cache';
import { pathForCamera, pathForFilmSimulation, pathForTag } from './paths';
import { formatCameraText } from '@/camera';
export default async function CommandK() {
const [
tags,
cameras,
filmSimulations,
] = await Promise.all([
getUniqueTagsCached().catch(() => []),
getUniqueCamerasCached().catch(() => []),
getUniqueFilmSimulationsCached().catch(() => []),
]);
return <CommandKClient
sections={[
{
heading: 'Pages',
items: [{
label: 'Home',
path: '/',
}, {
label: 'Grid',
path:'/grid',
}],
}, {
heading: 'Tags',
items: tags.map(({ tag }) => ({
label: tag,
path: pathForTag(tag),
})),
}, {
heading: 'Cameras',
items: cameras.map(({ camera }) => ({
label: formatCameraText(camera),
path: pathForCamera(camera),
})),
}, {
heading: 'Film Simulations',
items: filmSimulations.map(({ simulation }) => ({
label: simulation,
path: pathForFilmSimulation(simulation),
})),
},
]}
/>;
}