- {supporterData?.tier ? (
-
- ) : (
-
- Pangolin
-
- )}
-
-
- Fossorial
-
-
-
-
- Open Source
-
-
-
-
- Documentation
-
-
- {version && (
- <>
-
-
- v{version}
-
- >
- )}
+
diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx
new file mode 100644
index 00000000..bec0973f
--- /dev/null
+++ b/src/components/Breadcrumbs.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import { usePathname } from "next/navigation";
+import Link from "next/link";
+import { ChevronRight } from "lucide-react";
+import { cn } from "@app/lib/cn";
+
+interface BreadcrumbItem {
+ label: string;
+ href: string;
+}
+
+export function Breadcrumbs() {
+ const pathname = usePathname();
+ const segments = pathname.split("/").filter(Boolean);
+
+ const breadcrumbs: BreadcrumbItem[] = segments.map((segment, index) => {
+ const href = `/${segments.slice(0, index + 1).join("/")}`;
+ let label = segment;
+
+ // Format labels
+ if (segment === "settings") {
+ label = "Settings";
+ } else if (segment === "sites") {
+ label = "Sites";
+ } else if (segment === "resources") {
+ label = "Resources";
+ } else if (segment === "access") {
+ label = "Users & Roles";
+ } else if (segment === "general") {
+ label = "General";
+ } else if (segment === "share-links") {
+ label = "Shareable Links";
+ } else if (segment === "users") {
+ label = "Users";
+ } else if (segment === "roles") {
+ label = "Roles";
+ } else if (segment === "invitations") {
+ label = "Invitations";
+ } else if (segment === "connectivity") {
+ label = "Connectivity";
+ } else if (segment === "authentication") {
+ label = "Authentication";
+ }
+
+ return { label, href };
+ });
+
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 8b6e9ad2..6c0fc65a 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,160 +1,22 @@
"use client";
-import { Button } from "@app/components/ui/button";
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- CommandSeparator
-} from "@app/components/ui/command";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger
-} from "@app/components/ui/popover";
-import { useEnvContext } from "@app/hooks/useEnvContext";
-import { cn } from "@app/lib/cn";
-import { ListOrgsResponse } from "@server/routers/org";
-import { Check, ChevronsUpDown, Plus } from "lucide-react";
import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { useState } from "react";
-import { useUserContext } from "@app/hooks/useUserContext";
-import ProfileIcon from "./ProfileIcon";
-import SupporterStatus from "./SupporterStatus";
+import { useEnvContext } from "@app/hooks/useEnvContext";
-type HeaderProps = {
+interface HeaderProps {
orgId?: string;
- orgs?: ListOrgsResponse["orgs"];
-};
+ orgs?: any;
+}
export function Header({ orgId, orgs }: HeaderProps) {
- const { user, updateUser } = useUserContext();
-
- const [open, setOpen] = useState(false);
-
- const router = useRouter();
-
const { env } = useEnvContext();
return (
- <>
-
-
-
-
-
-
- {orgs && (
-
-
-
-
-
-
-
-
- No organizations found.
-
- {(!env.flags.disableUserCreateOrg ||
- user.serverAdmin) && (
- <>
-
-
- {
- router.push(
- "/setup"
- );
- }}
- >
-
- New Organization
-
-
-
-
- >
- )}
-
-
- {orgs.map((org) => (
- {
- router.push(
- `/${org.orgId}/settings`
- );
- }}
- >
-
- {org.name}
-
- ))}
-
-
-
-
-
- )}
-
-
- >
+
+
+ Pangolin
+
+
);
}
diff --git a/src/components/HorizontalTabs.tsx b/src/components/HorizontalTabs.tsx
new file mode 100644
index 00000000..f79b6fc8
--- /dev/null
+++ b/src/components/HorizontalTabs.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+import { useParams, usePathname } from "next/navigation";
+import { cn } from "@app/lib/cn";
+import { buttonVariants } from "@/components/ui/button";
+
+interface HorizontalTabsProps {
+ children: React.ReactNode;
+ items: Array<{
+ title: string;
+ href: string;
+ icon?: React.ReactNode;
+ }>;
+ disabled?: boolean;
+}
+
+export function HorizontalTabs({
+ children,
+ items,
+ disabled = false
+}: HorizontalTabsProps) {
+ const pathname = usePathname();
+ const params = useParams();
+
+ function hydrateHref(href: string) {
+ return href.replace("{orgId}", params.orgId as string);
+ }
+
+ return (
+
+
+
+
+ {items.map((item) => {
+ const hydratedHref = hydrateHref(item.href);
+ const isActive = pathname.startsWith(hydratedHref) && !pathname.includes("create");
+
+ return (
+
e.preventDefault() : undefined}
+ tabIndex={disabled ? -1 : undefined}
+ aria-disabled={disabled}
+ >
+ {item.icon ? (
+
+ {item.icon}
+ {item.title}
+
+ ) : (
+ item.title
+ )}
+
+ );
+ })}
+
+
+
+
+ {children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
new file mode 100644
index 00000000..9c327019
--- /dev/null
+++ b/src/components/Layout.tsx
@@ -0,0 +1,107 @@
+"use client";
+
+import React, { useState } from "react";
+import { Header } from "@app/components/Header";
+import { SidebarNav } from "@app/components/SidebarNav";
+import { TopBar } from "@app/components/TopBar";
+import { OrgSelector } from "@app/components/OrgSelector";
+import { cn } from "@app/lib/cn";
+import { ListOrgsResponse } from "@server/routers/org";
+import SupporterStatus from "@app/components/SupporterStatus";
+import { Separator } from "@app/components/ui/separator";
+import { Button } from "@app/components/ui/button";
+import { Menu, X } from "lucide-react";
+import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetDescription } from "@app/components/ui/sheet";
+import { useEnvContext } from "@app/hooks/useEnvContext";
+import { Breadcrumbs } from "@app/components/Breadcrumbs";
+
+interface LayoutProps {
+ children: React.ReactNode;
+ orgId?: string;
+ orgs?: ListOrgsResponse["orgs"];
+ navItems: Array<{
+ title: string;
+ href: string;
+ icon?: React.ReactNode;
+ children?: Array<{
+ title: string;
+ href: string;
+ icon?: React.ReactNode;
+ }>;
+ }>;
+}
+
+export function Layout({ children, orgId, orgs, navItems }: LayoutProps) {
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+ const { env } = useEnvContext();
+
+ return (
+
+ {/* Mobile Menu Button */}
+
+
+
+
+
+
+ Navigation Menu
+
+ Main navigation menu for the application
+
+
+
+
+
+
+
+
+
+
+ {env?.app?.version && (
+
+ v{env.app.version}
+
+ )}
+
+
+
+
+
+ {/* Desktop Sidebar */}
+
+
+
+
+
+
+
+
+
+
+ {env?.app?.version && (
+
+ v{env.app.version}
+
+ )}
+
+
+
+ {/* Main content */}
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/src/components/OrgSelector.tsx b/src/components/OrgSelector.tsx
new file mode 100644
index 00000000..c6a66725
--- /dev/null
+++ b/src/components/OrgSelector.tsx
@@ -0,0 +1,124 @@
+"use client";
+
+import { Button } from "@app/components/ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator
+} from "@app/components/ui/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger
+} from "@app/components/ui/popover";
+import { useEnvContext } from "@app/hooks/useEnvContext";
+import { cn } from "@app/lib/cn";
+import { ListOrgsResponse } from "@server/routers/org";
+import { Check, ChevronsUpDown, Plus } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useUserContext } from "@app/hooks/useUserContext";
+
+interface OrgSelectorProps {
+ orgId?: string;
+ orgs?: ListOrgsResponse["orgs"];
+}
+
+export function OrgSelector({ orgId, orgs }: OrgSelectorProps) {
+ const { user } = useUserContext();
+ const [open, setOpen] = useState(false);
+ const router = useRouter();
+ const { env } = useEnvContext();
+
+ return (
+
+
+
+
+
+
+
+
+ No organizations found.
+
+ {(!env.flags.disableUserCreateOrg ||
+ user.serverAdmin) && (
+ <>
+
+
+ {
+ router.push(
+ "/setup"
+ );
+ }}
+ >
+
+ New Organization
+
+
+
+
+ >
+ )}
+
+
+ {orgs?.map((org) => (
+ {
+ router.push(
+ `/${org.orgId}/settings`
+ );
+ }}
+ >
+
+ {org.name}
+
+ ))}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ProfileIcon.tsx b/src/components/ProfileIcon.tsx
index b8d13d7a..39f89377 100644
--- a/src/components/ProfileIcon.tsx
+++ b/src/components/ProfileIcon.tsx
@@ -66,7 +66,10 @@ export default function ProfileIcon() {
-
+
+
+ {user.email}
+
-
- {user.email}
-
-
-
-
>
);
diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx
index 20ef5bd1..318a8091 100644
--- a/src/components/SidebarNav.tsx
+++ b/src/components/SidebarNav.tsx
@@ -1,17 +1,9 @@
"use client";
-import React, { useEffect } from "react";
+import React from "react";
import Link from "next/link";
-import { useParams, usePathname, useRouter } from "next/navigation";
+import { useParams, usePathname } from "next/navigation";
import { cn } from "@app/lib/cn";
-import { buttonVariants } from "@/components/ui/button";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue
-} from "@/components/ui/select";
import { CornerDownRight } from "lucide-react";
interface SidebarNavItem {
@@ -39,43 +31,6 @@ export function SidebarNav({
const resourceId = params.resourceId as string;
const userId = params.userId as string;
- const [selectedValue, setSelectedValue] =
- React.useState
(getSelectedValue());
-
- useEffect(() => {
- setSelectedValue(getSelectedValue());
- }, [usePathname()]);
-
- const router = useRouter();
-
- const handleSelectChange = (value: string) => {
- if (!disabled) {
- router.push(value);
- }
- };
-
- function getSelectedValue() {
- let foundHref = "";
- for (const item of items) {
- const hydratedHref = hydrateHref(item.href);
- if (hydratedHref === pathname) {
- foundHref = hydratedHref;
- break;
- }
- if (item.children) {
- for (const child of item.children) {
- const hydratedChildHref = hydrateHref(child.href);
- if (hydratedChildHref === pathname) {
- foundHref = hydratedChildHref;
- break;
- }
- }
- }
- if (foundHref) break;
- }
- return foundHref;
- }
-
function hydrateHref(val: string): string {
return val
.replace("{orgId}", orgId)
@@ -85,142 +40,72 @@ export function SidebarNav({
}
function renderItems(items: SidebarNavItem[]) {
- return items.map((item) => (
-
-
e.preventDefault() : undefined}
- tabIndex={disabled ? -1 : undefined}
- aria-disabled={disabled}
- >
- {item.icon ? (
-
- {item.icon}
-
{item.title}
+ return items.map((item) => {
+ const hydratedHref = hydrateHref(item.href);
+ const isActive = pathname.startsWith(hydratedHref) && !pathname.includes("create");
+
+ return (
+
+
e.preventDefault() : undefined}
+ tabIndex={disabled ? -1 : undefined}
+ aria-disabled={disabled}
+ >
+ {item.icon &&
{item.icon}}
+ {item.title}
+
+ {item.children && (
+
+ {item.children.map((child) => {
+ const hydratedChildHref = hydrateHref(child.href);
+ const isChildActive = pathname.startsWith(hydratedChildHref) && !pathname.includes("create");
+
+ return (
+ e.preventDefault() : undefined}
+ tabIndex={disabled ? -1 : undefined}
+ aria-disabled={disabled}
+ >
+
+ {child.icon && {child.icon}}
+ {child.title}
+
+ );
+ })}
- ) : (
- item.title
)}
-
- {item.children && (
-
- {item.children.map((child) => (
-
-
e.preventDefault()
- : undefined
- }
- tabIndex={disabled ? -1 : undefined}
- aria-disabled={disabled}
- >
-
- {child.icon ? (
-
- {child.icon}
- {child.title}
-
- ) : (
- child.title
- )}
-
-
- ))}
-
- )}
-
- ));
+
+ );
+ });
}
return (
-
-
-
-
-
-
+
);
}
diff --git a/src/components/SupporterStatus.tsx b/src/components/SupporterStatus.tsx
index a08fdf77..baeeb545 100644
--- a/src/components/SupporterStatus.tsx
+++ b/src/components/SupporterStatus.tsx
@@ -419,7 +419,7 @@ export default function SupporterStatus() {
)
Table.displayName = "Table"
-const TableHeader = (
- {
- ref,
- className,
- ...props
- }: React.HTMLAttributes & {
- ref: React.RefObject;
- }
-) => ()
+const TableHeader = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
TableHeader.displayName = "TableHeader"
-const TableBody = (
- {
- ref,
- className,
- ...props
- }: React.HTMLAttributes & {
- ref: React.RefObject;
- }
-) => ()
+const TableBody = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
TableBody.displayName = "TableBody"
const TableFooter = (
@@ -67,55 +61,49 @@ const TableFooter = (
/>)
TableFooter.displayName = "TableFooter"
-const TableRow = (
- {
- ref,
- className,
- ...props
- }: React.HTMLAttributes & {
- ref: React.RefObject;
- }
-) => (
)
+const TableRow = React.forwardRef<
+ HTMLTableRowElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
TableRow.displayName = "TableRow"
-const TableHead = (
- {
- ref,
- className,
- ...props
- }: React.ThHTMLAttributes & {
- ref: React.RefObject;
- }
-) => ( | )
+const TableHead = React.forwardRef<
+ HTMLTableCellElement,
+ React.ThHTMLAttributes
+>(({ className, ...props }, ref) => (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+))
TableHead.displayName = "TableHead"
-const TableCell = (
- {
- ref,
- className,
- ...props
- }: React.TdHTMLAttributes & {
- ref: React.RefObject;
- }
-) => ( | )
+const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
+>(({ className, ...props }, ref) => (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+))
TableCell.displayName = "TableCell"
const TableCaption = (
diff --git a/tsconfig.json b/tsconfig.json
index 94729399..85c0e6f5 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -17,7 +17,8 @@
"@server/*": ["../server/*"],
"@test/*": ["../test/*"],
"@app/*": ["*"],
- "@/*": ["./*"]
+ "@/*": ["./*"],
+ "react": [ "./node_modules/@types/react" ]
},
"plugins": [
{
| |