Vectrum
Sign In
78 lines · 2.9 KB · TypeScript
import Link from "next/link";
import type { ReactNode } from "react";
import { Field } from "@unlocalhosted/metalui";
import { BookmarkIcon, HeartIcon, PlusIcon, RepoIcon, SearchIcon } from "./icons";
import { FieldIcon, FieldInput } from "./metal";
import { ButtonLink, Counter, Segmented, segmentClass } from "./ui";

export type ProfileTab = "repositories" | "lists" | "likes";

export const profileTab = (value: unknown): ProfileTab => (value === "lists" || value === "likes" ? value : "repositories");

const TABS: Record<ProfileTab, { label: string; icon: ReactNode; find: string }> = {
  repositories: { label: "Repositories", icon: <RepoIcon />, find: "Find a repository…" },
  lists: { label: "Lists", icon: <BookmarkIcon />, find: "Find a list…" },
  likes: { label: "Liked", icon: <HeartIcon />, find: "Find a repository…" },
};

/**
 * A profile's main column: its tabs (repositories, lists and, for people, the repositories they
 * like) with their counts, the search box, and a New key for what the tab shows.
 */
export function ProfileTabs({
  username,
  tab,
  counts,
  q,
  newHref,
  children,
}: {
  username: string;
  tab: ProfileTab;
  /** Tabs without a count aren't shown. */
  counts: Partial<Record<ProfileTab, number>>;
  q: string;
  newHref?: string | false | null;
  children: ReactNode;
}) {
  const tabs = (Object.keys(TABS) as ProfileTab[]).filter((t) => counts[t] !== undefined);
  return (
    <section>
      <div className="mb-5 flex flex-wrap items-center gap-3">
        <Segmented className="max-w-full overflow-x-auto">
          {tabs.map((t) => (
            <Link
              key={t}
              href={t === "repositories" ? `/${username}` : `/${username}?tab=${t}`}
              className={segmentClass(t === tab)}
              aria-current={t === tab ? "page" : undefined}
            >
              {TABS[t].icon} {TABS[t].label} <Counter>{counts[t]}</Counter>
            </Link>
          ))}
        </Segmented>
        <div className="ml-auto flex w-full items-center gap-2 sm:w-auto">
          <form role="search" className="flex-1 sm:w-72">
            {tab !== "repositories" && <input type="hidden" name="tab" value={tab} />}
            <Field size="regular">
              <FieldIcon>
                <SearchIcon />
              </FieldIcon>
              <FieldInput name="q" defaultValue={q} placeholder={TABS[tab].find} aria-label={TABS[tab].find.slice(0, -1)} />
            </Field>
          </form>
          {newHref && (
            <ButtonLink href={newHref} variant="primary" icon={<PlusIcon />}>
              New
            </ButtonLink>
          )}
        </div>
      </div>
      {children}
    </section>
  );
}

/** Whether a repository or list matches the profile's search. */
export const matches = (q: string, item: { name: string; description: string }) =>
  !q || item.name.toLowerCase().includes(q) || item.description.toLowerCase().includes(q);