Commit d1e18794 authored by Wohlgemuth, Jason's avatar Wohlgemuth, Jason
Browse files

feat: Replace kmenu with cmdk

parent efb693b5
Loading
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -35,7 +35,6 @@
    "clsx": "^2.1.1",
    "cmdk": "^1.0.0",
    "fuse.js": "^7.0.0",
    "kmenu": "^1.4.32",
    "lucide-react": "^0.441.0",
    "qrcodejs": "github:danielgjackson/qrcodejs",
    "react": "^18.3.1",
+37 −225
Original line number Diff line number Diff line
import 'kmenu/dist/index.css';
import * as Spinners from 'react-loader-spinner';
import {
    Breadcrumb,
    BreadcrumbItem,
    BreadcrumbLink,
    BreadcrumbList,
    BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import {
    CommandDialog,
    CommandEmpty,
@@ -14,45 +5,20 @@ import {
    CommandInput,
    CommandItem,
    CommandList,
    CommandSeparator,
} from '@/components/ui/command';
import {
    CommandMenu,
    CommandWrapper,
    MenuProvider,
    useCommands,
    useKmenu
} from 'kmenu';
import { useEffect, useState } from 'react';
import { BsQrCode } from 'react-icons/bs';
import { Button } from '@/components/ui/button';
import { CgSearchFound } from 'react-icons/cg';
import { Command } from 'lucide-react';
import Fuse from 'fuse.js';
import { GiOakLeaf } from 'react-icons/gi';
import { Command, Eye } from 'lucide-react';
import { Input } from '@/components/ui/input';
import QrCode from './QrCode.jsx';

const fuseOptions = {
    includeScore: true,
    isCaseSensitive: true,
    useExtendedSearch: false,
    keys: [
        'title',
        'subtitle',
        'sections.introduction',
        'sections.challenge',
        'sections.approach',
        'sections.outcomes',
        'sections.research.focus',
        'sections.research.areas',
        'contact.first',
        'contact.last',
    ],
};
const formatScoreText = value => {
    return value ? `Relevancy = ${(1.0 - value).toFixed(4)}` : '';
};
interface MenuItem {
    /* eslint-disable-next-line */
    children : React.ReactNode
    shortcut ?: string
    onSelect ?: (value : string) => void
}
const getParameters = key => {
    const search = window.location.search;
    const params = new URLSearchParams(search);
@@ -78,10 +44,6 @@ const updateLocation = key => {
        window.location.href = location;
    };
};
const LoadingSpinner = () => {
    const Spinner = Spinners.LineWave;
    return <Spinner />;
};
const ToggleButton = ({ onClick }) => {
    return (
        <Button id="catalog-toggle-palette" onClick={onClick}>
@@ -123,166 +85,21 @@ const SearchInput = () => {
        </div>
    );
};
const Palette = ({ items }) => {
    const [code, setCode] = useState('https://research.ornl.gov');
    const [visible, setVisible] = useState(false);
    const { input, open, setOpen, toggle } = useKmenu();
    const search = (items, value) => {
        const fuse = new Fuse(items, fuseOptions);
        const results = fuse.search(value).map(({ item, score }) => ({ ...item, score })).map(({ meta: { id, keywords }, score, title }) => ({
            text: `${title} (${formatScoreText(score)})`,
            icon: <CgSearchFound />,
            href: `/${id}`,
            keywords: keywords.join(' '),
            newTab: false
        }));
        const commands = {
            category: 'Results',
            commands: results
        };
        const resultsCommands = [
            {
                icon: <BsQrCode />,
                text: 'Generate QR Code',
                perform: () => {
                    setCode(`${window.origin}/?search=${value}`);
                    setVisible(true);
                }
            }
        ];
        return results.length > 0
            ? [
                {
                    category: '',
                    commands: resultsCommands
                },
                commands
            ]
            : [commands];
    };
    const main = [
        {
            category: 'Actions',
            commands: [
                {
                    icon: <BsQrCode />,
                    text: 'Generate QR Code',
                    perform: () => {
                        setCode(window.location.href);
                        setVisible(true);
                    }
                }
            ],
            subCommands: []
        },
        {
            category: 'Divisions',
            commands: []
        },
        {
            category: 'Groups',
            commands: []
        },
        {
            category: 'Projects',
            commands: items.map(({ meta: { id, keywords }, title: text }) => ({
                text,
                icon: <GiOakLeaf />,
                href: `/${id}`,
                keywords: keywords.join(' '),
                newTab: false
            }))
        },
    ];
    const loading = [];
    const [mainCommands] = useCommands(main);
    const [loadingCommands, setLoadingCommands] = useCommands(loading);
    const [awaiting, setAwaiting] = useState(true);
    useEffect(() => {
        if (open !== 2) {
            return;
        }
        setAwaiting(true);
        setLoadingCommands(search(items, input));
        setTimeout(() => setAwaiting(false), 1000);
    }, [input, open, setOpen]);
    return (
        <>
            <div id="control-container">
                <ToggleButton onClick={toggle} />
                <SearchInput />
            </div>
            <CommandWrapper>
                <CommandMenu
                    commands={mainCommands}
                    crumbs={['Home']}
                    index={1}
                    placeholder="Type something to filter..."
                />
                <CommandMenu
                    commands={loadingCommands}
                    crumbs={['Home', 'Search']}
                    index={2}
                    loadingState={awaiting}
                    loadingPlaceholder={<LoadingSpinner />}
                    placeholder="What are you looking for?"
                    preventSearch={true}
                />
            </CommandWrapper>
            <QrCode data={code} toggle={setVisible} visible={visible} />
        </>
    );
};
interface MenuItem {
    /* eslint-disable-next-line */
    children : React.ReactNode
    shortcut ?: string
    onSelect ?: (value : string) => void
}
function Item({ children, shortcut, onSelect = () => undefined } : MenuItem) {
    const style = {
        cursor: 'pointer',
        color: '#CCCCCC',
    };
    return (
        <CommandItem style={style} onSelect={onSelect}>
        <CommandItem onSelect={onSelect}>
            {children}
            {shortcut && <div cmdk-vercel-shortcuts="">{shortcut.split(' ').map(key => <kbd key={key}>{key}</kbd>)}</div>}
        </CommandItem>
    );
}
const Crumbs = ({ pages }) => {
    const style = {
        margin: '15px',
    };
    return (
        <Breadcrumb style={style}>
            <BreadcrumbList>
                {pages.map((page, index) => {
                    return (
                        <>
                            {index > 0 && <BreadcrumbSeparator />}
                            <BreadcrumbItem>
                                <BreadcrumbLink>{page}</BreadcrumbLink>
                            </BreadcrumbItem>
                        </>
                    );
                })}
            </BreadcrumbList>
        </Breadcrumb>
    );
};
const Projects = ({ items }) => {
    const action = {
        cursor: 'pointer',
        paddingLeft: '20px',
    };
    return (
        <CommandGroup heading="Projects">
        <CommandGroup heading="Research Activity Data">
            {items.map(item => (
                <Item key={item.title}>
                    <GiOakLeaf />
                    <span style={action}><a href={`/${item.meta.id}`}>{item.title}</a></span>
                    <Eye />
                    <span>&nbsp;<a href={`/${item.meta.identifier}`}>{item.title}</a></span>
                </Item>
            ))}
        </CommandGroup>
@@ -292,7 +109,6 @@ export default ({ items }) => {
    const [open, setOpen] = useState(false);
    const [code, setCode] = useState('https://research.ornl.gov');
    const [visible, setVisible] = useState(false);
    const [pages] = useState<string[]>(['Home']);
    const [, setInputValue] = useState('');
    useEffect(() => {
        const down = e => {
@@ -304,16 +120,13 @@ export default ({ items }) => {
        document.addEventListener('keydown', down);
        return () => document.removeEventListener('keydown', down);
    }, []);
    const action = {
        cursor: 'pointer',
        paddingLeft: '10px',
    };
    return (
        <MenuProvider>
            <Palette items={items} />
    return <div>
        <div id="control-container">
            <ToggleButton onClick={() => setOpen(true)} />
            <SearchInput />
        </div>
        <QrCode data={code} toggle={setVisible} visible={visible} />
        <CommandDialog open={open} onOpenChange={setOpen}>
                <Crumbs pages={pages} />
            <CommandInput placeholder="Type something to filter..." onValueChange={value => setInputValue(value)} />
            <CommandList>
                <CommandEmpty>No results found.</CommandEmpty>
@@ -325,13 +138,12 @@ export default ({ items }) => {
                    }}
                    >
                        <BsQrCode />
                            <span style={action}>Generate QR Code</span>
                        <span>&nbsp;&nbsp;Generate QR Code</span>
                    </CommandItem>
                </CommandGroup>
                    <CommandSeparator />
                <div style={{ height: '10px' }}></div>
                <Projects items={items} />
            </CommandList>
        </CommandDialog>
        </MenuProvider>
    );
    </div>;
};
+2 −6
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ const search = items => {
        keys: [
            'meta.identifier',
            'meta.media.caption',
            'meta.technology',
            'title',
            'subtitle',
            'sections.mission',
@@ -72,12 +73,7 @@ const Details = ({ items }) => {
    return (
        <div>
            <ChartNoAxesCombined style={{ display: 'inline-block', marginTop: '-2px', marginRight: '8px' }} />
            {visibleItems}
            {' '}
            of
            {count}
            {' '}
            items
            { [visibleItems, 'of', count, 'items'].join(' ') }
        </div>
    );
};
+1 −0
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ const QrCode = ({ data, label = 'qr-code', options = {}, size = 600, toggle, vis
            top: 0,
            visibility: visible ? 'visible' : 'hidden',
            width: '100%',
            zIndex: 9999,
        },
        caption: {
            color: '#CCCCCC',
+1 −3
Original line number Diff line number Diff line
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"

import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
@@ -26,7 +25,7 @@ interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
  return (
    <Dialog {...props}>
      <DialogContent className="overflow-hidden p-0 shadow-lg">
      <DialogContent className="overflow-hidden p-0 shadow-lg research-enablement">
        <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
          {children}
        </Command>
@@ -40,7 +39,6 @@ const CommandInput = React.forwardRef<
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
  <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
    <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
    <CommandPrimitive.Input
      ref={ref}
      className={cn(
Loading