← back to Open Seo

src/client/features/projects/ProjectSwitcher.tsx

111 lines

import { Link, useNavigate } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { Check, ChevronsUpDown, FolderCog } from "lucide-react";
import { getProjects } from "@/serverFunctions/projects";
import { setLastProjectId } from "@/client/lib/active-project";
import { closeDropdown } from "@/client/lib/dropdown";
import type { ProjectSummary } from "./types";

export function ProjectSwitcher({
  activeProjectId,
  onCloseDrawer,
}: {
  activeProjectId: string | null;
  // Mobile sidebar passes this so switching / navigating away also closes the
  // drawer overlay.
  onCloseDrawer?: () => void;
}) {
  const navigate = useNavigate();
  const projectsQuery = useQuery({
    queryKey: ["projects"],
    queryFn: () => getProjects(),
  });
  const projects = projectsQuery.data ?? [];
  const activeProject =
    projects.find((project) => project.id === activeProjectId) ?? null;

  const handleSelect = (project: ProjectSummary) => {
    closeDropdown();
    onCloseDrawer?.();
    if (project.id === activeProjectId) return;
    setLastProjectId(project.id);
    void navigate({
      to: "/p/$projectId",
      params: { projectId: project.id },
    });
  };

  return (
    <div className="dropdown w-full">
      <button
        type="button"
        tabIndex={0}
        aria-label="Switch project"
        className="flex w-full items-center justify-between gap-2 rounded-lg border border-base-300 bg-base-100 px-3 py-1.5 text-left transition-colors hover:border-base-content/25"
      >
        <span className="flex min-w-0 flex-col">
          <span className="truncate text-sm font-medium text-base-content">
            {activeProject?.name ?? "Select project"}
          </span>
          {activeProject?.domain ? (
            <span className="truncate text-xs font-normal text-base-content/50">
              {activeProject.domain}
            </span>
          ) : null}
        </span>
        <ChevronsUpDown className="size-3.5 shrink-0 text-base-content/40" />
      </button>

      <ul
        tabIndex={0}
        className="dropdown-content z-30 menu w-full rounded-box border border-base-300 bg-base-100 p-2 shadow-lg"
      >
        {projects.map((project) => {
          const isActive = project.id === activeProjectId;
          return (
            <li key={project.id}>
              <button
                type="button"
                onClick={() => handleSelect(project)}
                className={isActive ? "active" : ""}
              >
                <span className="flex min-w-0 flex-1 flex-col">
                  <span className="truncate">{project.name}</span>
                  {project.domain ? (
                    <span className="truncate text-xs text-base-content/50">
                      {project.domain}
                    </span>
                  ) : null}
                </span>
                {isActive ? (
                  <Check className="size-4 shrink-0 text-primary" />
                ) : null}
              </button>
            </li>
          );
        })}

        {projects.length > 0 ? (
          <li
            aria-hidden="true"
            className="pointer-events-none my-1 h-px bg-base-300 p-0"
          />
        ) : null}

        <li>
          <Link
            to="/projects"
            onClick={() => {
              closeDropdown();
              onCloseDrawer?.();
            }}
          >
            <FolderCog className="size-4" />
            Manage projects
          </Link>
        </li>
      </ul>
    </div>
  );
}