NobGit
public anord read

Ledger

Why work hard when you can work easier?

Languages

Repository composition by tracked source files.

TypeScript
TypeScript 86% CSS 10% SQL 3% Shell 1% HTML 0%
Create file Wiki Documentation
Clone
https://nobgit.com/orgs/anord/ledger.git
ssh://[email protected]:2222/orgs/anord/ledger.git
CommandSearch.tsx
import { useEffect, useRef, useState } from "react";
import { Link } from "react-router-dom";
import type { PageSummary } from "@ledger/shared";
import { Icon } from "./Icon";

type Space = {
  id: string;
  name: string;
  key: string;
  visibility: string;
};

export function CommandSearch({
  open,
  onClose,
  onSearch,
  spaces,
  results,
  isLoading
}: {
  open: boolean;
  onClose: () => void;
  onSearch: (query: string) => void;
  spaces: Space[];
  results: PageSummary[];
  isLoading: boolean;
}) {
  const [query, setQuery] = useState("");
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (!open) {
      setQuery("");
      return;
    }
    inputRef.current?.focus();
  }, [open]);

  useEffect(() => {
    if (!open) {
      return;
    }

    const timeout = window.setTimeout(() => {
      onSearch(query);
    }, 180);

    return () => window.clearTimeout(timeout);
  }, [open, onSearch, query]);

  useEffect(() => {
    function handleKeyDown(event: KeyboardEvent) {
      if (event.key === "Escape") {
        onClose();
      }
    }

    if (open) {
      window.addEventListener("keydown", handleKeyDown);
    }
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [onClose, open]);

  const grouped = results.reduce<Record<string, PageSummary[]>>((acc, result) => {
    acc[result.spaceId] = acc[result.spaceId] ?? [];
    acc[result.spaceId].push(result);
    return acc;
  }, {});

  const spaceNameFor = (spaceId: string) => spaces.find((space) => space.id === spaceId)?.name ?? "Collection";

  return (
    <>
      <div className={`command-palette__overlay${open ? " is-open" : ""}`} onClick={onClose} />
      <section className={`command-palette${open ? " is-open" : ""}`} aria-hidden={!open}>
        <div className="command-palette__input">
          <Icon name="search" className="icon" />
          <input
            ref={inputRef}
            value={query}
            onChange={(event) => setQuery(event.target.value)}
            placeholder="Search docs, pages, and answers"
            aria-label="Search"
          />
          <button type="button" className="button-ghost" onClick={onClose}>
            Esc
          </button>
        </div>

        <div className="command-palette__results">
          {isLoading ? <p className="muted">Searching...</p> : null}
          {!isLoading && query.trim() && results.length === 0 ? (
            <p className="muted">No results found.</p>
          ) : null}
          {!isLoading && !query.trim() ? (
            <p className="muted">Search by title or article content. Try a page name, keyword, or process.</p>
          ) : null}

          {Object.entries(grouped).map(([spaceId, items]) => (
            <div key={spaceId} className="search-group">
              <div className="search-group__label">{spaceNameFor(spaceId)}</div>
              {items.map((result) => (
                <Link key={result.id} to={`/page/${result.slug}`} className="search-result" onClick={onClose}>
                  <div className="search-result__icon">
                    <Icon name="document" className="icon icon-sm" />
                  </div>
                  <div className="search-result__body">
                    <strong>{result.title}</strong>
                    <p>{result.excerpt ?? "Open this article to read more."}</p>
                    <span>{spaceNameFor(spaceId)} / {result.slug}</span>
                  </div>
                </Link>
              ))}
            </div>
          ))}
        </div>
      </section>
    </>
  );
}