diff --git a/frontend/app/(sidebar)/dynamic-breadcrumb.tsx b/frontend/app/(sidebar)/dynamic-breadcrumb.tsx
new file mode 100644
index 0000000..00486a6
--- /dev/null
+++ b/frontend/app/(sidebar)/dynamic-breadcrumb.tsx
@@ -0,0 +1,47 @@
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb";
+import React from "react";
+
+export interface DynamicBreadcrumbProps {
+ pathname: string;
+}
+
+export default function DynamicBreadcrumb({ pathname }: DynamicBreadcrumbProps) {
+ const segments = pathname.split("/").filter(Boolean);
+
+ const breadcrumbItems = segments.map((segment, index) => {
+ const href = "/" + segments.slice(0, index + 1).join("/");
+ const title = segment.charAt(0).toUpperCase() + segment.slice(1);
+ return { title, href };
+ });
+
+ return (
+
+
+ {breadcrumbItems.map((item, index) => {
+ const isLast = index === breadcrumbItems.length - 1;
+ return (
+
+ {isLast ? (
+
+ {item.title}
+
+ ) : (
+
+ {item.title}
+
+ )}
+ {index < breadcrumbItems.length - 1 && }
+
+ );
+ })}
+
+
+ );
+}
diff --git a/frontend/app/(sidebar)/layout.tsx b/frontend/app/(sidebar)/layout.tsx
index d391981..3e79b13 100644
--- a/frontend/app/(sidebar)/layout.tsx
+++ b/frontend/app/(sidebar)/layout.tsx
@@ -1,21 +1,21 @@
+"use client";
+
import { AppSidebar } from "@/components/sidebar/app-sidebar";
import { ThemeToggle } from "@/components/theme-toggle";
-import {
- Breadcrumb,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbPage,
- BreadcrumbSeparator,
-} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
+import DynamicBreadcrumb from "./dynamic-breadcrumb";
+import { extractRoute } from "@/lib/utils";
+import { usePathname } from "next/navigation";
export default function AppLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
+ const pathname = usePathname();
+ const currentPathname = extractRoute(pathname);
+
return (
@@ -25,17 +25,7 @@ export default function AppLayout({
-
-
-
- Building Your Application
-
-
-
- Data Fetching
-
-
-
+
{children}
diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts
index bd0c391..e0dedc9 100644
--- a/frontend/lib/utils.ts
+++ b/frontend/lib/utils.ts
@@ -1,6 +1,22 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
+}
+
+/**
+ * Given a pathname string, returns a cleaned route path by removing numeric segments.
+ *
+ * For example, "/farms/1/crops/2" becomes "/farms/crops".
+ *
+ * @param pathname A pathname such as "/farms/1/crops/2"
+ * @returns A cleaned pathname string starting with a "/"
+ */
+export function extractRoute(pathname: string): string {
+ // Split the pathname into segments and remove any empty segments.
+ const segments = pathname.split("/").filter(Boolean);
+ // Remove segments which are entirely numeric.
+ const nonNumericSegments = segments.filter((segment) => isNaN(Number(segment)));
+ return "/" + nonNumericSegments.join("/");
}
diff --git a/frontend/package.json b/frontend/package.json
index 193ef85..e7de5e8 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,10 +16,13 @@
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
+ "@radix-ui/react-progress": "^1.1.2",
+ "@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
+ "@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
"@react-google-maps/api": "^2.20.6",
"@tailwindcss/typography": "^0.5.16",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 2c858e0..271ea63 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -29,6 +29,12 @@ importers:
'@radix-ui/react-label':
specifier: ^2.1.2
version: 2.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-progress':
+ specifier: ^1.1.2
+ version: 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-scroll-area':
+ specifier: ^1.2.3
+ version: 1.2.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-select':
specifier: ^2.1.6
version: 2.1.6(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -41,6 +47,9 @@ importers:
'@radix-ui/react-switch':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-tabs':
+ specifier: ^1.1.3
+ version: 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-tooltip':
specifier: ^1.1.8
version: 1.1.8(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -671,6 +680,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-progress@1.1.2':
+ resolution: {integrity: sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-roving-focus@1.1.2':
resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==}
peerDependencies:
@@ -684,6 +706,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-scroll-area@1.2.3':
+ resolution: {integrity: sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-select@2.1.6':
resolution: {integrity: sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==}
peerDependencies:
@@ -732,6 +767,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-tabs@1.1.3':
+ resolution: {integrity: sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-tooltip@1.1.8':
resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==}
peerDependencies:
@@ -2925,6 +2973,16 @@ snapshots:
'@types/react': 19.0.8
'@types/react-dom': 19.0.3(@types/react@19.0.8)
+ '@radix-ui/react-progress@1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ dependencies:
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ optionalDependencies:
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+
'@radix-ui/react-roving-focus@1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
@@ -2942,6 +3000,23 @@ snapshots:
'@types/react': 19.0.8
'@types/react-dom': 19.0.3(@types/react@19.0.8)
+ '@radix-ui/react-scroll-area@1.2.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ dependencies:
+ '@radix-ui/number': 1.1.0
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-direction': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ optionalDependencies:
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+
'@radix-ui/react-select@2.1.6(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/number': 1.1.0
@@ -3002,6 +3077,22 @@ snapshots:
'@types/react': 19.0.8
'@types/react-dom': 19.0.3(@types/react@19.0.8)
+ '@radix-ui/react-tabs@1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-direction': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-id': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-roving-focus': 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.0.0)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ optionalDependencies:
+ '@types/react': 19.0.8
+ '@types/react-dom': 19.0.3(@types/react@19.0.8)
+
'@radix-ui/react-tooltip@1.1.8(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1