Seed basic command-k data
This commit is contained in:
parent
fdf53111cf
commit
1dd0ea9101
@ -48,6 +48,7 @@
|
|||||||
"sonner": "^1.4.0",
|
"sonner": "^1.4.0",
|
||||||
"tailwindcss": "3.4.1",
|
"tailwindcss": "3.4.1",
|
||||||
"ts-exif-parser": "^0.2.2",
|
"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:
|
typescript:
|
||||||
specifier: 5.3.3
|
specifier: 5.3.3
|
||||||
version: 5.3.3
|
version: 5.3.3
|
||||||
|
use-debounce:
|
||||||
|
specifier: ^10.0.0
|
||||||
|
version: 10.0.0(react@18.2.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -7569,6 +7572,15 @@ packages:
|
|||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: false
|
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):
|
/use-sidecar@1.1.2(@types/react@18.2.55)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
|
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import Footer from '@/site/Footer';
|
|||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import FooterClient from '@/site/FooterClient';
|
import FooterClient from '@/site/FooterClient';
|
||||||
import NavClient from '@/site/NavClient';
|
import NavClient from '@/site/NavClient';
|
||||||
import CommandK from '@/components/CommandK';
|
import CommandK from '@/site/CommandK';
|
||||||
|
|
||||||
import '../site/globals.css';
|
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({
|
export default function Modal({
|
||||||
onClosePath,
|
onClosePath,
|
||||||
onClose,
|
onClose,
|
||||||
|
className,
|
||||||
|
anchor = 'center',
|
||||||
children,
|
children,
|
||||||
fast,
|
fast,
|
||||||
}: {
|
}: {
|
||||||
onClosePath?: string
|
onClosePath?: string
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
|
className?: string
|
||||||
|
anchor?: 'top' | 'center'
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
fast?: boolean
|
fast?: boolean
|
||||||
}) {
|
}) {
|
||||||
@ -51,7 +55,10 @@ export default function Modal({
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className={clsx(
|
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',
|
'bg-black',
|
||||||
)}
|
)}
|
||||||
initial={!prefersReducedMotion
|
initial={!prefersReducedMotion
|
||||||
@ -69,6 +76,7 @@ export default function Modal({
|
|||||||
'bg-white dark:bg-black',
|
'bg-white dark:bg-black',
|
||||||
'dark:border dark:border-gray-800',
|
'dark:border dark:border-gray-800',
|
||||||
'md:p-4 md:rounded-xl',
|
'md:p-4 md:rounded-xl',
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
style={{ width: 'min(500px, 90vw)' }}
|
style={{ width: 'min(500px, 90vw)' }}
|
||||||
ref={contentRef}
|
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