Vectrum
Sign In
93 lines · 3.7 KB · TypeScript
"use client";

import { useRouter } from "next/navigation";
import { useEffect, useState, useSyncExternalStore } from "react";
import { CommandPalette, SearchField, type CommandPaletteItem } from "@unlocalhosted/metalui";
import { CompassIcon, GearIcon, HomeIcon, KeyIcon, PlusIcon, SearchIcon, ShieldLockIcon } from "./icons";
import { RepoAvatar } from "./ui";

const noSubscribe = () => () => {};
const isApple = () => /Mac|iPhone|iPad/.test(navigator.platform);

export type PaletteRepo = { owner: string; name: string; imageVersion: number | null };

/**
 * The header's search well (MetalUI's SearchField) and the ⌘K palette it opens: jump to a repository,
 * go to a place, or search every repository for the words typed. ⌘K / Ctrl+K or "/" opens it anywhere.
 */
export function SearchBox({ repos = [], signedIn = false }: { repos?: PaletteRepo[]; signedIn?: boolean }) {
  const router = useRouter();
  const apple = useSyncExternalStore(noSubscribe, isApple, () => true);
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState("");

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      const target = e.target as HTMLElement;
      const typing = target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName);
      const shortcut = (e.key === "k" && (e.metaKey || e.ctrlKey)) || (e.key === "/" && !typing);
      if (!shortcut) return;
      e.preventDefault();
      setOpen(true);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  const q = query.trim();
  const items: CommandPaletteItem[] = [
    ...(q ? [{ id: `search:${q}`, section: "SEARCH", label: `Search repositories for “${q}”`, icon: <SearchIcon />, hint: "EXPLORE" }] : []),
    ...repos.map((r) => ({
      id: `repo:${r.owner}/${r.name}`,
      section: "REPOSITORIES",
      label: `${r.owner}/${r.name}`,
      icon: <RepoAvatar repo={r} size={14} />,
    })),
    { id: "go:/", section: "PLACES", label: "Overview", icon: <HomeIcon /> },
    { id: "go:/explore", section: "PLACES", label: "Explore", icon: <CompassIcon />, keywords: ["browse", "discover"] },
    ...(signedIn
      ? [
          { id: "go:/new", section: "ACTIONS", label: "New Repository", icon: <PlusIcon />, keywords: ["create"] },
          { id: "go:/settings/profile", section: "ACTIONS", label: "Settings", icon: <GearIcon />, keywords: ["profile", "account"] },
          { id: "go:/settings/keys", section: "ACTIONS", label: "SSH Keys", icon: <KeyIcon /> },
          { id: "go:/settings/tokens", section: "ACTIONS", label: "API Tokens", icon: <ShieldLockIcon /> },
        ]
      : []),
  ];

  return (
    <>
      <SearchField
        placeholder="Search"
        icon={<SearchIcon />}
        shortcut={apple ? "⌘K" : "Ctrl K"}
        aria-keyshortcuts={apple ? "Meta+K" : "Control+K"}
        tone="light"
        onClick={() => setOpen(true)}
        className="hidden md:flex"
      />
      <CommandPalette
        open={open}
        onOpenChange={(next) => {
          setOpen(next);
          if (!next) setQuery("");
        }}
        query={query}
        onQueryChange={setQuery}
        items={items}
        icon={<SearchIcon />}
        placeholder="Repository, place or words to search"
        status={signedIn ? "YOUR REPOSITORIES" : "PUBLIC REPOSITORIES"}
        empty="Nothing matches"
        aria-label="Search"
        onRun={(item) => {
          const [kind, ...rest] = item.id.split(":");
          const value = rest.join(":");
          if (kind === "search") router.push(`/explore?q=${encodeURIComponent(value)}`);
          else if (kind === "repo") router.push(`/${value}`);
          else router.push(value);
        }}
      />
    </>
  );
}