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";
0b4b5829import { DropdownItem } from "@/app/components/ui/dropdown";
0b4b58210import { NavBar } from "@/app/components/ui/navbar";
10621c511import { useAppSwitcherItems } from "@/lib/use-app-switcher";
bc2f20512
d9e329513function useIsProductHost() {
d9e329514 const [isProductHost, setIsProductHost] = useState<boolean | null>(null);
da0f65115 useEffect(() => {
d9e329516 const hostname = window.location.hostname;
3cbdca617 setIsProductHost(
bf6031c18 hostname.startsWith("canopy.") || hostname.startsWith("ring.") || hostname.startsWith("collab.")
3cbdca619 );
da0f65120 }, []);
d9e329521 return isProductHost;
da0f65122}
da0f65123
4a006da24export function Nav() {
818dc9025 const params = useParams<{ owner?: string; repo?: string }>();
818dc9026 const owner = params?.owner;
818dc9027 const repo = params?.repo;
4dfd09b28 const pathname = usePathname();
d9e329529 const isProductHost = useIsProductHost();
6dd74de30 const { user } = useAuth();
6dd74de31 const [isRepoOwner, setIsRepoOwner] = useState(false);
10621c532 const appSwitcherItems = useAppSwitcherItems("grove", { owner, repo });
da0f65133
6dd74de34 // Determine if the current user owns this repo (directly or via org membership)
6dd74de35 useEffect(() => {
6dd74de36 if (!owner || !repo || !user) {
6dd74de37 setIsRepoOwner(false);
6dd74de38 return;
6dd74de39 }
6dd74de40 // Quick check: if the URL owner matches the username, they own it
6dd74de41 if (owner === user.username) {
6dd74de42 setIsRepoOwner(true);
6dd74de43 return;
6dd74de44 }
6dd74de45 // Otherwise check org membership
6dd74de46 orgsApi
6dd74de47 .get(owner)
6dd74de48 .then(({ members }) => {
6dd74de49 setIsRepoOwner(members.some((m) => m.username === user.username));
6dd74de50 })
6dd74de51 .catch(() => setIsRepoOwner(false));
6dd74de52 }, [owner, repo, user]);
6dd74de53
0b4b58254 // Don't render Grove nav on product subdomains
d9e329555 if (isProductHost === null || isProductHost) return null;
4dfd09b56
4dfd09b57 const repoTabs = owner && repo ? [
8a2c7d458 { key: "code", label: "Code", shortLabel: "Code", href: `/${owner}/${repo}` },
d12933e59 { key: "diffs", label: "Diffs", shortLabel: "Diffs", href: `/${owner}/${repo}/diffs` },
8a2c7d460 { key: "commits", label: "Commits", shortLabel: "Commits", href: `/${owner}/${repo}/commits` },
6dd74de61 ...(isRepoOwner ? [{ key: "settings", label: "Settings", shortLabel: "Settings", href: `/${owner}/${repo}/settings` }] : []),
4dfd09b62 ] : null;
4dfd09b63
4dfd09b64 const repoPrefix = owner && repo ? `/${owner}/${repo}` : "";
4dfd09b65 const repoPath = pathname?.startsWith(repoPrefix) ? pathname.slice(repoPrefix.length) : "";
4dfd09b66 const activeTab =
4dfd09b67 repoPath.startsWith("/commit") ? "commits"
d12933e68 : repoPath.startsWith("/diffs") ? "diffs"
ab61b9d69 : repoPath.startsWith("/settings") ? "settings"
4dfd09b70 : "code";
4dfd09b71
4dfd09b72 // Parse file/tree path breadcrumbs from URL like /blob/main/src/lib/utils.ts
4dfd09b73 let pathBreadcrumbs: { name: string; href: string; isLast: boolean }[] = [];
4dfd09b74 const blobMatch = repoPath.match(/^\/(blob|tree)\/([^/]+)\/(.+)$/);
4dfd09b75 if (blobMatch && owner && repo) {
4dfd09b76 const [, routeType, ref, filePath] = blobMatch;
4dfd09b77 const parts = filePath.split("/");
4dfd09b78 pathBreadcrumbs = parts.map((part, i) => {
4dfd09b79 const partPath = parts.slice(0, i + 1).join("/");
4dfd09b80 const isLast = i === parts.length - 1;
4dfd09b81 const type = isLast && routeType === "blob" ? "blob" : "tree";
4dfd09b82 return {
d744b8283 name: decodeURIComponent(part),
4dfd09b84 href: `/${owner}/${repo}/${type}/${ref}/${partPath}`,
4dfd09b85 isLast,
4dfd09b86 };
4dfd09b87 });
4dfd09b88 }
4a006da89
4a006da90 return (
0b4b58291 <NavBar
0b4b58292 logo={<GroveLogo size={28} />}
0b4b58293 productName="Grove"
0b4b58294 showProductName={!owner}
0b4b58295 breadcrumbs={
0b4b58296 <>
818dc9097 {owner && (
818dc9098 <>
818dc9099 <span style={{ color: "var(--text-faint)" }}>/</span>
818dc90100 <Link
818dc90101 href={`/${owner}`}
818dc90102 className="hover:underline truncate"
818dc90103 style={{ color: "var(--text-muted)" }}
818dc90104 >
818dc90105 {owner}
818dc90106 </Link>
818dc90107 </>
818dc90108 )}
818dc90109 {owner && repo && (
818dc90110 <>
818dc90111 <span style={{ color: "var(--text-faint)" }}>/</span>
818dc90112 <Link
818dc90113 href={`/${owner}/${repo}`}
818dc90114 className="hover:underline truncate"
4dfd09b115 style={{ color: pathBreadcrumbs.length > 0 ? "var(--text-muted)" : "var(--text-primary)" }}
818dc90116 >
818dc90117 {repo}
818dc90118 </Link>
818dc90119 </>
818dc90120 )}
4dfd09b121 {pathBreadcrumbs.map((crumb) => (
10621c5122 <span key={crumb.href} className={`hidden sm:flex items-center gap-1.5 sm:gap-3`}>
4dfd09b123 <span style={{ color: "var(--text-faint)" }}>/</span>
4dfd09b124 {crumb.isLast ? (
4dfd09b125 <span style={{ color: "var(--text-primary)" }}>{crumb.name}</span>
4dfd09b126 ) : (
4dfd09b127 <Link
4dfd09b128 href={crumb.href}
4dfd09b129 className="hover:underline"
4dfd09b130 style={{ color: "var(--text-muted)" }}
4dfd09b131 >
4dfd09b132 {crumb.name}
4dfd09b133 </Link>
4dfd09b134 )}
4dfd09b135 </span>
4dfd09b136 ))}
0b4b582137 </>
0b4b582138 }
10621c5139 appSwitcherItems={appSwitcherItems}
0b4b582140 menuItems={
27902ea141 <>
27902ea142 <DropdownItem onClick={() => { window.location.href = "/dashboard"; }}>
27902ea143 Dashboard
27902ea144 </DropdownItem>
27902ea145 <DropdownItem onClick={() => { window.location.href = "/settings"; }}>
27902ea146 Settings
27902ea147 </DropdownItem>
27902ea148 </>
0b4b582149 }
0b4b582150 belowBar={
0b4b582151 repoTabs ? (
10621c5152 <div className="flex px-3 sm:px-6 gap-0 text-sm overflow-x-auto overflow-y-hidden">
0b4b582153 {repoTabs.map((tab) => {
0b4b582154 const isActive = tab.key === activeTab;
0b4b582155 return (
0b4b582156 <Link
0b4b582157 key={tab.key}
0b4b582158 href={tab.href}
10621c5159 className="px-3 py-2 -mb-px transition-colors shrink-0"
10621c5160 style={{
10621c5161 color: isActive ? "var(--text-primary)" : "var(--text-muted)",
10621c5162 borderBottom: isActive
10621c5163 ? "2px solid var(--accent)"
10621c5164 : "2px solid transparent",
10621c5165 }}
0b4b582166 >
0b4b582167 {tab.label}
0b4b582168 </Link>
0b4b582169 );
0b4b582170 })}
bdf6540171 </div>
0b4b582172 ) : undefined
0b4b582173 }
0b4b582174 />
4a006da175 );
4a006da176}