web/app/nav.tsxblame
View source
4a006da1"use client";
4a006da2
10621c53import { useState, useEffect } from "react";
818dc904import Link from "next/link";
4dfd09b5import { useParams, usePathname } from "next/navigation";
10621c56import { orgs as orgsApi } from "@/lib/api";
6dd74de7import { useAuth } from "@/lib/auth";
818dc908import { GroveLogo } from "@/app/components/grove-logo";
44863ab9import { ThemeToggle } from "@/app/theme-toggle";
0b4b58210import { DropdownItem } from "@/app/components/ui/dropdown";
0b4b58211import { NavBar } from "@/app/components/ui/navbar";
10621c512import { useAppSwitcherItems } from "@/lib/use-app-switcher";
bc2f20513
d9e329514function useIsProductHost() {
d9e329515 const [isProductHost, setIsProductHost] = useState<boolean | null>(null);
da0f65116 useEffect(() => {
d9e329517 const hostname = window.location.hostname;
3cbdca618 setIsProductHost(
bf6031c19 hostname.startsWith("canopy.") || hostname.startsWith("ring.") || hostname.startsWith("collab.")
3cbdca620 );
da0f65121 }, []);
d9e329522 return isProductHost;
da0f65123}
da0f65124
4a006da25export function Nav() {
818dc9026 const params = useParams<{ owner?: string; repo?: string }>();
818dc9027 const owner = params?.owner;
818dc9028 const repo = params?.repo;
4dfd09b29 const pathname = usePathname();
d9e329530 const isProductHost = useIsProductHost();
6dd74de31 const { user } = useAuth();
6dd74de32 const [isRepoOwner, setIsRepoOwner] = useState(false);
10621c533 const appSwitcherItems = useAppSwitcherItems("grove", { owner, repo });
da0f65134
6dd74de35 // Determine if the current user owns this repo (directly or via org membership)
6dd74de36 useEffect(() => {
6dd74de37 if (!owner || !repo || !user) {
6dd74de38 setIsRepoOwner(false);
6dd74de39 return;
6dd74de40 }
6dd74de41 // Quick check: if the URL owner matches the username, they own it
6dd74de42 if (owner === user.username) {
6dd74de43 setIsRepoOwner(true);
6dd74de44 return;
6dd74de45 }
6dd74de46 // Otherwise check org membership
6dd74de47 orgsApi
6dd74de48 .get(owner)
6dd74de49 .then(({ members }) => {
6dd74de50 setIsRepoOwner(members.some((m) => m.username === user.username));
6dd74de51 })
6dd74de52 .catch(() => setIsRepoOwner(false));
6dd74de53 }, [owner, repo, user]);
6dd74de54
0b4b58255 // Don't render Grove nav on product subdomains
d9e329556 if (isProductHost === null || isProductHost) return null;
4dfd09b57
4dfd09b58 const repoTabs = owner && repo ? [
8a2c7d459 { key: "code", label: "Code", shortLabel: "Code", href: `/${owner}/${repo}` },
d12933e60 { key: "diffs", label: "Diffs", shortLabel: "Diffs", href: `/${owner}/${repo}/diffs` },
8a2c7d461 { key: "commits", label: "Commits", shortLabel: "Commits", href: `/${owner}/${repo}/commits` },
6dd74de62 ...(isRepoOwner ? [{ key: "settings", label: "Settings", shortLabel: "Settings", href: `/${owner}/${repo}/settings` }] : []),
4dfd09b63 ] : null;
4dfd09b64
4dfd09b65 const repoPrefix = owner && repo ? `/${owner}/${repo}` : "";
4dfd09b66 const repoPath = pathname?.startsWith(repoPrefix) ? pathname.slice(repoPrefix.length) : "";
4dfd09b67 const activeTab =
4dfd09b68 repoPath.startsWith("/commit") ? "commits"
d12933e69 : repoPath.startsWith("/diffs") ? "diffs"
ab61b9d70 : repoPath.startsWith("/settings") ? "settings"
4dfd09b71 : "code";
4dfd09b72
4dfd09b73 // Parse file/tree path breadcrumbs from URL like /blob/main/src/lib/utils.ts
4dfd09b74 let pathBreadcrumbs: { name: string; href: string; isLast: boolean }[] = [];
4dfd09b75 const blobMatch = repoPath.match(/^\/(blob|tree)\/([^/]+)\/(.+)$/);
4dfd09b76 if (blobMatch && owner && repo) {
4dfd09b77 const [, routeType, ref, filePath] = blobMatch;
4dfd09b78 const parts = filePath.split("/");
4dfd09b79 pathBreadcrumbs = parts.map((part, i) => {
4dfd09b80 const partPath = parts.slice(0, i + 1).join("/");
4dfd09b81 const isLast = i === parts.length - 1;
4dfd09b82 const type = isLast && routeType === "blob" ? "blob" : "tree";
4dfd09b83 return {
d744b8284 name: decodeURIComponent(part),
4dfd09b85 href: `/${owner}/${repo}/${type}/${ref}/${partPath}`,
4dfd09b86 isLast,
4dfd09b87 };
4dfd09b88 });
4dfd09b89 }
4a006da90
4a006da91 return (
0b4b58292 <NavBar
0b4b58293 logo={<GroveLogo size={28} />}
0b4b58294 productName="Grove"
0b4b58295 showProductName={!owner}
0b4b58296 breadcrumbs={
0b4b58297 <>
818dc9098 {owner && (
818dc9099 <>
818dc90100 <span style={{ color: "var(--text-faint)" }}>/</span>
818dc90101 <Link
818dc90102 href={`/${owner}`}
818dc90103 className="hover:underline truncate"
818dc90104 style={{ color: "var(--text-muted)" }}
818dc90105 >
818dc90106 {owner}
818dc90107 </Link>
818dc90108 </>
818dc90109 )}
818dc90110 {owner && repo && (
818dc90111 <>
818dc90112 <span style={{ color: "var(--text-faint)" }}>/</span>
818dc90113 <Link
818dc90114 href={`/${owner}/${repo}`}
818dc90115 className="hover:underline truncate"
4dfd09b116 style={{ color: pathBreadcrumbs.length > 0 ? "var(--text-muted)" : "var(--text-primary)" }}
818dc90117 >
818dc90118 {repo}
818dc90119 </Link>
818dc90120 </>
818dc90121 )}
4dfd09b122 {pathBreadcrumbs.map((crumb) => (
10621c5123 <span key={crumb.href} className={`hidden sm:flex items-center gap-1.5 sm:gap-3`}>
4dfd09b124 <span style={{ color: "var(--text-faint)" }}>/</span>
4dfd09b125 {crumb.isLast ? (
4dfd09b126 <span style={{ color: "var(--text-primary)" }}>{crumb.name}</span>
4dfd09b127 ) : (
4dfd09b128 <Link
4dfd09b129 href={crumb.href}
4dfd09b130 className="hover:underline"
4dfd09b131 style={{ color: "var(--text-muted)" }}
4dfd09b132 >
4dfd09b133 {crumb.name}
4dfd09b134 </Link>
4dfd09b135 )}
4dfd09b136 </span>
4dfd09b137 ))}
0b4b582138 </>
0b4b582139 }
44863ab140 actions={!user ? <ThemeToggle /> : undefined}
10621c5141 appSwitcherItems={appSwitcherItems}
0b4b582142 menuItems={
27902ea143 <>
27902ea144 <DropdownItem onClick={() => { window.location.href = "/dashboard"; }}>
27902ea145 Dashboard
27902ea146 </DropdownItem>
27902ea147 <DropdownItem onClick={() => { window.location.href = "/settings"; }}>
27902ea148 Settings
27902ea149 </DropdownItem>
27902ea150 </>
0b4b582151 }
0b4b582152 belowBar={
0b4b582153 repoTabs ? (
10621c5154 <div className="flex px-3 sm:px-6 gap-0 text-sm overflow-x-auto overflow-y-hidden">
0b4b582155 {repoTabs.map((tab) => {
0b4b582156 const isActive = tab.key === activeTab;
0b4b582157 return (
0b4b582158 <Link
0b4b582159 key={tab.key}
0b4b582160 href={tab.href}
10621c5161 className="px-3 py-2 -mb-px transition-colors shrink-0"
10621c5162 style={{
10621c5163 color: isActive ? "var(--text-primary)" : "var(--text-muted)",
10621c5164 borderBottom: isActive
10621c5165 ? "2px solid var(--accent)"
10621c5166 : "2px solid transparent",
10621c5167 }}
0b4b582168 >
0b4b582169 {tab.label}
0b4b582170 </Link>
0b4b582171 );
0b4b582172 })}
bdf6540173 </div>
0b4b582174 ) : undefined
0b4b582175 }
0b4b582176 />
4a006da177 );
4a006da178}