Add navigation indicators

This commit is contained in:
bkellam 2025-10-16 20:38:08 -07:00
parent d0cb69fdbe
commit 88705f5e7e
2 changed files with 100 additions and 61 deletions

View file

@ -1,24 +1,22 @@
import { getRepos, getReposStats } from "@/actions"; import { getRepos, getReposStats } from "@/actions";
import { SourcebotLogo } from "@/app/components/sourcebotLogo"; import { SourcebotLogo } from "@/app/components/sourcebotLogo";
import { auth } from "@/auth"; import { auth } from "@/auth";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { NavigationMenu as NavigationMenuBase, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu"; import { NavigationMenu as NavigationMenuBase } from "@/components/ui/navigation-menu";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { getSubscriptionInfo } from "@/ee/features/billing/actions"; import { getSubscriptionInfo } from "@/ee/features/billing/actions";
import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
import { ServiceErrorException } from "@/lib/serviceError"; import { ServiceErrorException } from "@/lib/serviceError";
import { cn, getShortenedNumberDisplayString, isServiceError } from "@/lib/utils"; import { isServiceError } from "@/lib/utils";
import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons"; import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
import { RepoJobStatus, RepoJobType } from "@sourcebot/db"; import { RepoJobStatus, RepoJobType } from "@sourcebot/db";
import { BookMarkedIcon, CircleIcon, MessageCircleIcon, SearchIcon, SettingsIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { OrgSelector } from "../orgSelector"; import { OrgSelector } from "../orgSelector";
import { SettingsDropdown } from "../settingsDropdown"; import { SettingsDropdown } from "../settingsDropdown";
import WhatsNewIndicator from "../whatsNewIndicator"; import WhatsNewIndicator from "../whatsNewIndicator";
import { NavigationItems } from "./navigationItems";
import { ProgressIndicator } from "./progressIndicator"; import { ProgressIndicator } from "./progressIndicator";
import { TrialIndicator } from "./trialIndicator"; import { TrialIndicator } from "./trialIndicator";
@ -70,7 +68,7 @@ export const NavigationMenu = async ({
return ( return (
<div className="flex flex-col w-full h-fit bg-background"> <div className="flex flex-col w-full h-fit bg-background">
<div className="flex flex-row justify-between items-center py-1.5 px-3"> <div className="flex flex-row justify-between items-center py-0.5 px-3">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<Link <Link
href={`/${domain}`} href={`/${domain}`}
@ -92,61 +90,12 @@ export const NavigationMenu = async ({
)} )}
<NavigationMenuBase> <NavigationMenuBase>
<NavigationMenuList className="gap-2"> <NavigationItems
<NavigationMenuItem> domain={domain}
<NavigationMenuLink numberOfRepos={numberOfRepos}
href={`/${domain}`} numberOfReposWithFirstTimeIndexingJobsInProgress={numberOfReposWithFirstTimeIndexingJobsInProgress}
className={cn(navigationMenuTriggerStyle(), "gap-2")} isAuthenticated={isAuthenticated}
> />
<SearchIcon className="w-4 h-4 mr-1" />
Search
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
href={`/${domain}/chat`}
className={navigationMenuTriggerStyle()}
>
<MessageCircleIcon className="w-4 h-4 mr-1" />
Ask
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem className="relative">
<Tooltip>
<TooltipTrigger asChild>
<NavigationMenuLink
href={`/${domain}/repos`}
className={navigationMenuTriggerStyle()}
>
<BookMarkedIcon className="w-4 h-4 mr-1" />
<span className="mr-2">Repositories</span>
<Badge variant="secondary" className="px-1.5 relative">
{getShortenedNumberDisplayString(numberOfRepos)}
{numberOfReposWithFirstTimeIndexingJobsInProgress > 0 && (
<CircleIcon className="absolute -right-0.5 -top-0.5 h-2 w-2 text-green-600" fill="currentColor" />
)}
</Badge>
</NavigationMenuLink>
</TooltipTrigger>
<TooltipContent>
<p>{numberOfRepos} total {numberOfRepos === 1 ? 'repository' : 'repositories'}</p>
</TooltipContent>
</Tooltip>
</NavigationMenuItem>
{isAuthenticated && (
<>
<NavigationMenuItem>
<NavigationMenuLink
href={`/${domain}/settings`}
className={navigationMenuTriggerStyle()}
>
<SettingsIcon className="w-4 h-4 mr-1" />
Settings
</NavigationMenuLink>
</NavigationMenuItem>
</>
)}
</NavigationMenuList>
</NavigationMenuBase> </NavigationMenuBase>
</div> </div>

View file

@ -0,0 +1,90 @@
"use client";
import { NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
import { Badge } from "@/components/ui/badge";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn, getShortenedNumberDisplayString } from "@/lib/utils";
import { SearchIcon, MessageCircleIcon, BookMarkedIcon, SettingsIcon, CircleIcon } from "lucide-react";
import { usePathname } from "next/navigation";
interface NavigationItemsProps {
domain: string;
numberOfRepos: number;
numberOfReposWithFirstTimeIndexingJobsInProgress: number;
isAuthenticated: boolean;
}
export const NavigationItems = ({
domain,
numberOfRepos,
numberOfReposWithFirstTimeIndexingJobsInProgress,
isAuthenticated,
}: NavigationItemsProps) => {
const pathname = usePathname();
const isActive = (href: string) => {
if (href === `/${domain}`) {
return pathname === `/${domain}`;
}
return pathname.startsWith(href);
};
return (
<NavigationMenuList className="gap-2">
<NavigationMenuItem className="relative">
<NavigationMenuLink
href={`/${domain}`}
className={cn(navigationMenuTriggerStyle(), "gap-2")}
>
<SearchIcon className="w-4 h-4 mr-1" />
Search
</NavigationMenuLink>
{isActive(`/${domain}`) && <ActiveIndicator />}
</NavigationMenuItem>
<NavigationMenuItem className="relative">
<NavigationMenuLink
href={`/${domain}/chat`}
className={navigationMenuTriggerStyle()}
>
<MessageCircleIcon className="w-4 h-4 mr-1" />
Ask
</NavigationMenuLink>
{isActive(`/${domain}/chat`) && <ActiveIndicator />}
</NavigationMenuItem>
<NavigationMenuItem className="relative">
<NavigationMenuLink
href={`/${domain}/repos`}
className={navigationMenuTriggerStyle()}
>
<BookMarkedIcon className="w-4 h-4 mr-1" />
<span className="mr-2">Repositories</span>
<Badge variant="secondary" className="px-1.5 relative">
{getShortenedNumberDisplayString(numberOfRepos)}
{numberOfReposWithFirstTimeIndexingJobsInProgress > 0 && (
<CircleIcon className="absolute -right-0.5 -top-0.5 h-2 w-2 text-green-600" fill="currentColor" />
)}
</Badge>
</NavigationMenuLink>
{isActive(`/${domain}/repos`) && <ActiveIndicator />}
</NavigationMenuItem>
{isAuthenticated && (
<NavigationMenuItem className="relative">
<NavigationMenuLink
href={`/${domain}/settings`}
className={navigationMenuTriggerStyle()}
>
<SettingsIcon className="w-4 h-4 mr-1" />
Settings
</NavigationMenuLink>
{isActive(`/${domain}/settings`) && <ActiveIndicator />}
</NavigationMenuItem>
)}
</NavigationMenuList>
);
};
const ActiveIndicator = () => {
return (
<div className="absolute -bottom-2 left-0 right-0 h-0.5 bg-foreground" />
);
};