Seed basic command-k data
This commit is contained in:
parent
fdf53111cf
commit
1dd0ea9101
@ -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
12
pnpm-lock.yaml
generated
@ -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'}
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
116
src/components/CommandKClient.tsx
Normal file
116
src/components/CommandKClient.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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
53
src/site/CommandK.tsx
Normal 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),
|
||||
})),
|
||||
},
|
||||
]}
|
||||
/>;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user