devist 0.8.0

Project bootstrap CLI for AI-assisted development. Spin up new projects from templates, manage backends, and keep your codebase comprehensible.
import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext";
import { listProjects } from "@/lib/queries";
import type { ProjectSummary } from "@/types";
import { Activity, Inbox, LogOut, ScrollText } from "lucide-react";
import { useEffect, useState } from "react";
import { NavLink, Outlet, useNavigate } from "react-router-dom";

export default function DashboardLayout() {
  const { session, signOut } = useAuth();
  const navigate = useNavigate();
  const [projects, setProjects] = useState<ProjectSummary[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let alive = true;
    listProjects()
      .then((list) => {
        if (alive) {
          setProjects(list);
          setLoading(false);
        }
      })
      .catch((e) => {
        if (alive) {
          setError(String(e?.message ?? e));
          setLoading(false);
        }
      });
    return () => {
      alive = false;
    };
  }, []);

  async function handleSignOut() {
    await signOut();
    navigate("/", { replace: true });
  }

  return (
    <div className="min-h-screen bg-background text-foreground flex">
      <aside className="w-64 border-r flex flex-col">
        <div className="p-4 border-b">
          <div className="text-sm font-bold">devist dashboard</div>
          <div className="text-xs text-muted-foreground truncate">{session?.user.email}</div>
        </div>

        <nav className="p-2 space-y-1 text-sm">
          <NavItem to="/dashboard" icon={<Activity size={14} />}>
            Overview
          </NavItem>
          <NavItem to="/dashboard/inbox" icon={<Inbox size={14} />}>
            Inbox
          </NavItem>
          <NavItem to="/dashboard/rules" icon={<ScrollText size={14} />}>
            Rules
          </NavItem>
        </nav>

        <div className="px-4 pt-4 pb-1 text-[11px] uppercase tracking-wide text-muted-foreground">
          Projects
        </div>
        <div className="flex-1 overflow-y-auto px-2 pb-2 space-y-0.5 text-sm">
          {loading && <div className="text-xs text-muted-foreground px-3 py-2">Loading…</div>}
          {error && <div className="text-xs text-red-600 px-3 py-2">{error}</div>}
          {!loading && !error && projects.length === 0 && (
            <div className="text-xs text-muted-foreground px-3 py-2">
              No events yet. Run <code className="font-mono">devist worker start</code>.
            </div>
          )}
          {projects.map((p) => (
            <NavLink
              key={p.project}
              to={`/dashboard/projects/${encodeURIComponent(p.project)}`}
              className={({ isActive }) =>
                `flex items-center justify-between px-3 py-1.5 rounded-md hover:bg-muted ${
                  isActive ? "bg-muted font-medium" : ""
                }`
              }
            >
              <span className="truncate">{p.project}</span>
              <span className="text-xs text-muted-foreground">{p.event_count}</span>
            </NavLink>
          ))}
        </div>

        <div className="border-t p-2">
          <Button
            variant="ghost"
            size="sm"
            className="w-full justify-start gap-2"
            onClick={handleSignOut}
          >
            <LogOut size={14} />
            Sign out
          </Button>
        </div>
      </aside>

      <main className="flex-1 overflow-y-auto">
        <Outlet />
      </main>
    </div>
  );
}

function NavItem({
  to,
  icon,
  children,
}: {
  to: string;
  icon: React.ReactNode;
  children: React.ReactNode;
}) {
  return (
    <NavLink
      to={to}
      end
      className={({ isActive }) =>
        `flex items-center gap-2 px-3 py-1.5 rounded-md hover:bg-muted ${
          isActive ? "bg-muted font-medium" : ""
        }`
      }
    >
      {icon}
      {children}
    </NavLink>
  );
}