Skip to content

Commit

Permalink
🪛(fix): breadcrumb not appearing on certain routes (#273)
Browse files Browse the repository at this point in the history
* 🪛(fix): route matching

For example if routing to `/accounting/division-codes` the breadcrumb would not appear, but would appear if you routed to `/accounting/division-codes/`

* 🍪(chore): formatting

* 🪛(fix): route inconsistencies

* 🟡(change): breadcrumb to shadcn component

* bunp

* bump

* bump
  • Loading branch information
emoss08 committed Jul 21, 2024
1 parent e0d8c88 commit 744f2d6
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 67 deletions.
105 changes: 66 additions & 39 deletions web/frontend/src/components/layout/breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,57 +15,74 @@
* Grant, and not modifying the license in any other way.
*/

import { upperFirst } from "@/lib/utils";
import { toTitleCase } from "@/lib/utils";
import { routes } from "@/routing/AppRoutes";
import { useBreadcrumbStore } from "@/stores/BreadcrumbStore";
import { pathToRegexp } from "path-to-regexp";
import { useEffect, useMemo } from "react";
import { useLocation } from "react-router-dom";
import { Link, matchPath, useLocation } from "react-router-dom";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "../ui/breadcrumb";
import { Skeleton } from "../ui/skeleton";
import { FavoriteIcon } from "./user-favorite";

const useRouteMatching = (
setLoading: (loading: boolean) => void,
setCurrentRoute: (route: any) => void,
) => {
type BreadcrumbItemType = {
label: string;
path: string;
};
export function SiteBreadcrumb({ children }: { children?: React.ReactNode }) {
const location = useLocation();
const [currentRoute, setCurrentRoute] =
useBreadcrumbStore.use("currentRoute");
const [loading, setLoading] = useBreadcrumbStore.use("loading");

const matchingRoute = routes.find(
(route) => route.path !== "*" && matchPath(route.path, location.pathname),
);

useEffect(() => {
setLoading(true);
const matchedRoute = routes.find((route) => {
return (
route.path !== "*" && pathToRegexp(route.path).test(location.pathname)
);
});

setCurrentRoute(matchedRoute || null);
setCurrentRoute(matchingRoute || null);
setLoading(false);
}, [location, setLoading, setCurrentRoute]);
};
}, [location, setCurrentRoute, setLoading]);

const useDocumentTitle = (currentRoute: any) => {
useEffect(() => {
if (currentRoute) {
document.title = currentRoute.title;
}
}, [currentRoute]);
};
const breadcrumbItems = useMemo(() => {
if (!currentRoute) return [];
const items: BreadcrumbItemType[] = [{ label: "Home", path: "/" }];

export function Breadcrumb({ children }: { children?: React.ReactNode }) {
const [currentRoute, setCurrentRoute] =
useBreadcrumbStore.use("currentRoute");
const [loading, setLoading] = useBreadcrumbStore.use("loading");
useRouteMatching(setLoading, setCurrentRoute);
useDocumentTitle(currentRoute);
if (currentRoute.group) {
items.push({
label: toTitleCase(currentRoute.group),
path: `/${currentRoute.group}`,
});
}

// Construct breadcrumb text
const breadcrumbText = useMemo(() => {
if (!currentRoute) return "";
const parts = [currentRoute.group, currentRoute.subMenu, currentRoute.title]
.filter((str): str is string => str !== undefined)
.map(upperFirst);
return parts.join(" - ");
}, [currentRoute]);
if (currentRoute.subMenu) {
items.push({
label: toTitleCase(currentRoute.subMenu),
path: currentRoute.group
? `/${currentRoute.group}/${currentRoute.subMenu}`
: `/${currentRoute.subMenu}`,
});
}

items.push({
label: toTitleCase(currentRoute.title),
path: location.pathname,
});

return items;
}, [currentRoute, location.pathname]);

if (loading) {
return (
Expand All @@ -76,7 +93,6 @@ export function Breadcrumb({ children }: { children?: React.ReactNode }) {
);
}

// If the current route is not found or is an excluded path, return null
if (!currentRoute) {
return null;
}
Expand All @@ -85,14 +101,25 @@ export function Breadcrumb({ children }: { children?: React.ReactNode }) {
<div className="pb-4 pt-5 md:py-4">
<div>
<h2 className="mt-10 flex scroll-m-20 items-center pb-2 text-xl font-semibold tracking-tight transition-colors first:mt-0">
{currentRoute?.title}
{toTitleCase(currentRoute.title)}
<FavoriteIcon />
</h2>
<div className="flex items-center">
<a className="text-sm font-medium text-muted-foreground hover:text-muted-foreground/80">
{breadcrumbText}
</a>
</div>
<Breadcrumb>
<BreadcrumbList>
{breadcrumbItems.map((item, index) => (
<BreadcrumbItem key={item.path}>
{index === breadcrumbItems.length - 1 ? (
<BreadcrumbPage>{item.label}</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link to={item.path}>{item.label}</Link>
</BreadcrumbLink>
)}
{index < breadcrumbItems.length - 1 && <BreadcrumbSeparator />}
</BreadcrumbItem>
))}
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="mt-3 flex">{children}</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions web/frontend/src/components/layout/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { useUserStore } from "@/stores/AuthStore";
import React from "react";
import { useLocation } from "react-router-dom";
import MainAsideMenu, { AsideMenuDialog } from "./aside-menu";
import { Breadcrumb } from "./breadcrumb";
import { SiteBreadcrumb } from "./breadcrumb";
import { NotificationMenu } from "./notification_menu/notification-menu";
import { SiteSearchDialog } from "./site-search-dialog";

Expand Down Expand Up @@ -68,7 +68,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
</header>
)}
<main className="flex-1 overflow-auto px-6">
<Breadcrumb />
<SiteBreadcrumb />
<SiteSearchDialog />
{children}
</main>
Expand Down
22 changes: 9 additions & 13 deletions web/frontend/src/components/layout/site-search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
*/

import { useHeaderStore } from "@/stores/HeaderStore";
import {
MagnifyingGlassIcon
} from "@radix-ui/react-icons";
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
import { Button } from "../ui/button";
import { KeyCombo, Keys, ShortcutsProvider } from "../ui/keyboard";
import {
Expand All @@ -28,7 +26,6 @@ import {
TooltipTrigger,
} from "../ui/tooltip";


export function SearchButton() {
return (
<TooltipProvider delayDuration={100}>
Expand All @@ -40,9 +37,9 @@ export function SearchButton() {
aria-label="Open site search"
aria-expanded={useHeaderStore.get("searchDialogOpen")}
onClick={() => useHeaderStore.set("searchDialogOpen", true)}
className="group relative flex size-8 border-muted-foreground/40 hover:border-muted-foreground/80"
className="border-muted-foreground/40 hover:border-muted-foreground/80 group relative flex size-8"
>
<MagnifyingGlassIcon className="size-5 text-muted-foreground group-hover:text-foreground" />
<MagnifyingGlassIcon className="text-muted-foreground group-hover:text-foreground size-5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={5}>
Expand All @@ -53,25 +50,24 @@ export function SearchButton() {
);
}


export function SiteSearchInput() {
return (
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<span
aria-label="Open site search"
aria-expanded={useHeaderStore.get('searchDialogOpen')}
onClick={() => useHeaderStore.set('searchDialogOpen', true)}
className="group mt-10 hidden h-9 w-[250px] items-center justify-between rounded-md border border-muted-foreground/20 px-3 py-2 text-sm hover:cursor-pointer hover:border-muted-foreground/80 hover:bg-accent xl:flex"
aria-expanded={useHeaderStore.get("searchDialogOpen")}
onClick={() => useHeaderStore.set("searchDialogOpen", true)}
className="border-muted-foreground/20 hover:border-muted-foreground/80 hover:bg-accent group mt-10 hidden h-9 w-[250px] items-center justify-between rounded-md border px-3 py-2 text-sm hover:cursor-pointer xl:flex"
>
<div className="flex items-center">
<MagnifyingGlassIcon className="mr-2 size-5 text-muted-foreground group-hover:text-foreground" />
<MagnifyingGlassIcon className="text-muted-foreground group-hover:text-foreground mr-2 size-5" />
<span className="text-muted-foreground">Search...</span>
</div>
<div className="pointer-events-none inline-flex select-none">
<ShortcutsProvider os="mac">
<KeyCombo keyNames={[Keys.Command, 'K']} />
<KeyCombo keyNames={[Keys.Command, "K"]} />
</ShortcutsProvider>
</div>
</span>
Expand All @@ -82,4 +78,4 @@ export function SiteSearchInput() {
</Tooltip>
</TooltipProvider>
);
}
}
115 changes: 115 additions & 0 deletions web/frontend/src/components/ui/breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"

import { cn } from "@/lib/utils"

const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"

const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-zinc-500 sm:gap-2.5 dark:text-zinc-400",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"

const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"

const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"

return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-zinc-950 dark:hover:text-zinc-50", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"

const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-zinc-950 dark:text-zinc-50", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"

const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"

const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"

export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
12 changes: 5 additions & 7 deletions web/frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,14 +219,12 @@ export function shipmentStatusToReadable(status: ShipmentStatus) {
}
}

export const toTitleCase = (value: string) => {
return value
.toLowerCase()
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
export function toTitleCase(str: string): string {
return str
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
};

}
export const focusRing = [
// base
"outline outline-offset-2 outline-0 focus-visible:outline-2",
Expand Down
Loading

0 comments on commit 744f2d6

Please sign in to comment.