first
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Building2,
|
||||
FolderKanban,
|
||||
Bell,
|
||||
Search,
|
||||
ChevronDown,
|
||||
UserCircle,
|
||||
} from "lucide-react";
|
||||
import { orgs, projects } from "@/lib/api";
|
||||
import { Dropdown, type Option } from "@/components/ui/Dropdown";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { useAppStore } from "@/stores/useAppStore";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Topbar({ className }: { className?: string }) {
|
||||
const setOrg = useAppStore((s) => s.setOrg);
|
||||
const setProject = useAppStore((s) => s.setProject);
|
||||
const currentOrg = useAppStore((s) => s.org_id);
|
||||
const currentProject = useAppStore((s) => s.project_id);
|
||||
const openAsf = useAppStore((s) => s.openAsf);
|
||||
|
||||
const { data: orgList = [] } = useQuery({
|
||||
queryKey: ["orgs"],
|
||||
queryFn: () => orgs.list({ limit: 50 }),
|
||||
initialData: [],
|
||||
});
|
||||
|
||||
const { data: projectList = [] } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: () => projects.list({ limit: 100 }),
|
||||
initialData: [],
|
||||
enabled: !!currentOrg,
|
||||
});
|
||||
|
||||
const orgOptions: Option<string>[] = orgList.map((o) => ({
|
||||
value: o.id,
|
||||
label: o.name,
|
||||
description: o.slug,
|
||||
}));
|
||||
if (orgOptions.length === 0) {
|
||||
// Dev-mode synthetic org (matches NEXT_PUBLIC_DEV_TENANT_ID)
|
||||
orgOptions.push({
|
||||
value: currentOrg || "00000000-0000-0000-0000-000000000001",
|
||||
label: "Azurian Labs",
|
||||
description: "azurian-labs (bootstrap demo)",
|
||||
});
|
||||
}
|
||||
|
||||
const projectOptions: Option<string>[] = projectList.map((p) => ({
|
||||
value: p.id,
|
||||
label: `${p.key} · ${p.name}`,
|
||||
description: p.description || undefined,
|
||||
}));
|
||||
// Add a synthetic "All projects" option
|
||||
projectOptions.unshift({
|
||||
value: "__all__",
|
||||
label: "All projects",
|
||||
description: "Tenant-wide view",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProject && projectList.length > 0) {
|
||||
setProject(projectList[0].id);
|
||||
}
|
||||
}, [currentProject, projectList, setProject]);
|
||||
|
||||
return (
|
||||
<header className={cn(
|
||||
"h-16 shrink-0 border-b border-slate-200/80 bg-white/80 backdrop-blur",
|
||||
"flex items-center gap-4 px-4 md:px-6",
|
||||
className,
|
||||
)}>
|
||||
{/* Mobile brand */}
|
||||
<div className="lg:hidden flex items-center gap-2 font-semibold text-slate-800">
|
||||
<div className="w-8 h-8 rounded-md bg-gradient-to-br from-brand-500 to-accent text-white grid place-items-center text-sm font-bold">
|
||||
A
|
||||
</div>
|
||||
ASF
|
||||
</div>
|
||||
|
||||
<div className="flex-1 max-w-xl hidden md:block">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openAsf}
|
||||
className="w-full group flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-slate-500
|
||||
bg-slate-50 hover:bg-slate-100 border border-slate-200 transition-colors text-left"
|
||||
>
|
||||
<Search className="w-4 h-4 text-slate-400 group-hover:text-slate-600" />
|
||||
<span className="flex-1">Search artifacts, projects, requirements… or press <span className="font-medium text-slate-600">⌘ K</span></span>
|
||||
<kbd className="asf-chip !text-[10px]">Search</kbd>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2 md:gap-3">
|
||||
<Dropdown
|
||||
label="Organization"
|
||||
size="sm"
|
||||
icon={<Building2 className="w-4 h-4" />}
|
||||
options={orgOptions}
|
||||
value={currentOrg}
|
||||
onChange={(v) => {
|
||||
setOrg(v);
|
||||
setProject(undefined);
|
||||
}}
|
||||
/>
|
||||
<Dropdown
|
||||
label="Project"
|
||||
size="sm"
|
||||
icon={<FolderKanban className="w-4 h-4" />}
|
||||
options={projectOptions}
|
||||
value={currentProject === undefined ? "__all__" : currentProject}
|
||||
onChange={(v) => setProject(v === "__all__" ? undefined : v)}
|
||||
/>
|
||||
<Button variant="ghost" size="md" aria-label="Notifications">
|
||||
<Bell className="w-4.5 h-4.5 w-[18px] h-[18px]" />
|
||||
</Button>
|
||||
<div className="h-9 w-9 rounded-full bg-slate-100 border border-slate-200 grid place-items-center text-slate-600">
|
||||
<UserCircle className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user