move improvements to layout

This commit is contained in:
miloschwartz 2025-04-18 11:36:34 -04:00
parent 8c0e4d2d8c
commit 3bab90891f
No known key found for this signature in database
9 changed files with 199 additions and 218 deletions

View file

@ -114,7 +114,7 @@ export async function listUsers(
const [{ count }] = await db const [{ count }] = await db
.select({ count: sql<number>`count(*)` }) .select({ count: sql<number>`count(*)` })
.from(users) .from(userOrgs)
.where(eq(userOrgs.orgId, orgId)); .where(eq(userOrgs.orgId, orgId));
return response<ListUsersResponse>(res, { return response<ListUsersResponse>(res, {

View file

@ -1,6 +1,13 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
import { Figtree, Inter, Red_Hat_Display, Red_Hat_Mono, Red_Hat_Text, Space_Grotesk } from "next/font/google"; import {
Figtree,
Inter,
Red_Hat_Display,
Red_Hat_Mono,
Red_Hat_Text,
Space_Grotesk
} from "next/font/google";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { ThemeProvider } from "@app/providers/ThemeProvider"; import { ThemeProvider } from "@app/providers/ThemeProvider";
import EnvProvider from "@app/providers/EnvProvider"; import EnvProvider from "@app/providers/EnvProvider";

View file

@ -47,24 +47,22 @@ export function Breadcrumbs() {
}); });
return ( return (
<div className="border-b px-4 py-2 overflow-x-auto scrollbar-hide bg-card"> <nav className="flex items-center space-x-1 text-muted-foreground">
<nav className="flex items-center space-x-1 text-sm text-muted-foreground whitespace-nowrap"> {breadcrumbs.map((crumb, index) => (
{breadcrumbs.map((crumb, index) => ( <div key={crumb.href} className="flex items-center flex-nowrap">
<div key={crumb.href} className="flex items-center whitespace-nowrap"> {index !== 0 && <ChevronRight className="h-4 w-4 flex-shrink-0" />}
{index !== 0 && <ChevronRight className="h-4 w-4" />} <Link
<Link href={crumb.href}
href={crumb.href} className={cn(
className={cn( "ml-1 hover:text-foreground whitespace-nowrap",
"ml-1 hover:text-foreground whitespace-nowrap", index === breadcrumbs.length - 1 &&
index === breadcrumbs.length - 1 && "text-foreground font-medium"
"text-foreground font-medium" )}
)} >
> {crumb.label}
{crumb.label} </Link>
</Link> </div>
</div> ))}
))} </nav>
</nav>
</div>
); );
} }

View file

@ -1,30 +0,0 @@
"use client";
import Link from "next/link";
import { useEnvContext } from "@app/hooks/useEnvContext";
import Image from "next/image";
interface HeaderProps {
orgId?: string;
orgs?: any;
}
export function Header({ orgId, orgs }: HeaderProps) {
const { env } = useEnvContext();
return (
<div className="flex items-center justify-between w-full">
<Link href="/" className="flex items-center space-x-2">
<Image
src="/logo/pangolin_orange.svg"
alt="Pangolin Logo"
width={34}
height={34}
/>
<span className="font-[Space_Grotesk] font-bold text-2xl text-neutral-500">Pangolin</span>
</Link>
</div>
);
}
export default Header;

View file

@ -1,16 +1,15 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState } from "react";
import { Header } from "@app/components/Header";
import { SidebarNav } from "@app/components/SidebarNav"; import { SidebarNav } from "@app/components/SidebarNav";
import { TopBar } from "@app/components/TopBar";
import { OrgSelector } from "@app/components/OrgSelector"; import { OrgSelector } from "@app/components/OrgSelector";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { ListOrgsResponse } from "@server/routers/org"; import { ListOrgsResponse } from "@server/routers/org";
import SupporterStatus from "@app/components/SupporterStatus"; import SupporterStatus from "@app/components/SupporterStatus";
import { Separator } from "@app/components/ui/separator";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { ExternalLink, Menu, X, Server } from "lucide-react"; import { ExternalLink, Menu, X, Server } from "lucide-react";
import Image from "next/image";
import ProfileIcon from "@app/components/ProfileIcon";
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
@ -61,139 +60,185 @@ export function Layout({
const { user } = useUserContext(); const { user } = useUserContext();
return ( return (
<div className="flex h-screen overflow-hidden"> <div className="flex flex-col h-screen overflow-hidden">
{/* Mobile Menu Button */} {/* Full width header */}
{showSidebar && ( {showHeader && (
<div className="md:hidden fixed top-4 left-4 z-50"> <div className="border-b shrink-0 bg-card">
<Sheet <div className="h-16 flex items-center px-4">
open={isMobileMenuOpen} <div className="flex items-center gap-4">
onOpenChange={setIsMobileMenuOpen} {showSidebar && (
> <div className="md:hidden">
<SheetTrigger asChild> <Sheet
<Button variant="ghost" size="icon"> open={isMobileMenuOpen}
<Menu className="h-6 w-6" /> onOpenChange={setIsMobileMenuOpen}
</Button> >
</SheetTrigger> <SheetTrigger asChild>
<SheetContent <Button variant="ghost" size="icon">
side="left" <Menu className="h-6 w-6" />
className="w-64 p-0 flex flex-col h-full" </Button>
> </SheetTrigger>
<SheetTitle className="sr-only"> <SheetContent
Navigation Menu side="left"
</SheetTitle> className="w-64 p-0 flex flex-col h-full"
<SheetDescription className="sr-only"> >
Main navigation menu for the application <SheetTitle className="sr-only">
</SheetDescription> Navigation Menu
{showHeader && ( </SheetTitle>
<div className="flex h-16 items-center border-b px-4 shrink-0"> <SheetDescription className="sr-only">
<Header orgId={orgId} orgs={orgs} /> Main navigation menu for the
application
</SheetDescription>
<div className="flex-1 overflow-y-auto">
<div className="p-4">
<SidebarNav
items={navItems}
onItemClick={() =>
setIsMobileMenuOpen(
false
)
}
/>
</div>
{!isAdminPage && (
<div className="p-4 border-t">
<Link
href="/admin"
className="flex items-center gap-3 text-muted-foreground hover:text-foreground transition-colors px-3 py-2 rounded-md w-full"
onClick={() =>
setIsMobileMenuOpen(false)
}
>
<Server className="h-4 w-4" />
Server Admin
</Link>
</div>
)}
</div>
<div className="p-4 space-y-4 border-t shrink-0">
<SupporterStatus />
<OrgSelector
orgId={orgId}
orgs={orgs}
/>
{env?.app?.version && (
<div className="text-xs text-muted-foreground text-center">
v{env.app.version}
</div>
)}
</div>
</SheetContent>
</Sheet>
</div> </div>
)} )}
<div className="flex-1 overflow-y-auto p-4"> <Link
<SidebarNav href="/"
items={navItems} className="flex items-center hidden md:block"
onItemClick={() => >
setIsMobileMenuOpen(false) <Image
} src="/logo/pangolin_orange.svg"
alt="Pangolin Logo"
width={35}
height={35}
/> />
{!isAdminPage && ( </Link>
<div className="mt-8 pt-4 border-t"> {showBreadcrumbs && (
<Link <div className="hidden md:block overflow-x-auto scrollbar-hide">
href="/admin" <Breadcrumbs />
className="flex items-center justify-center gap-2 text-sm font-medium text-primary hover:text-primary/80 transition-colors px-4 py-2 rounded-md bg-primary/10 hover:bg-primary/20 w-full" </div>
onClick={() => )}
setIsMobileMenuOpen(false) </div>
} {showTopBar && (
> <div className="ml-auto flex items-center justify-end md:justify-between">
<Server className="h-4 w-4" /> <div className="hidden md:flex items-center space-x-3 mr-6">
Server Admin <Link
</Link> href="https://docs.fossorial.io"
</div> target="_blank"
)} rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Documentation
</Link>
<Link
href="mailto:support@fossorial.io"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Support
</Link>
</div>
<div>
<ProfileIcon />
</div>
</div> </div>
<div className="p-4 space-y-4 border-t shrink-0"> )}
<SupporterStatus /> </div>
<OrgSelector orgId={orgId} orgs={orgs} /> {showBreadcrumbs && (
<div className="md:hidden px-4 pb-2 overflow-x-auto scrollbar-hide">
<Breadcrumbs />
</div>
)}
</div>
)}
<div className="flex flex-1 overflow-hidden">
{/* Desktop Sidebar */}
{showSidebar && (
<div className="hidden md:flex w-64 border-r bg-card flex-col h-full shrink-0">
<div className="flex-1 overflow-y-auto">
<div className="p-4">
<SidebarNav items={navItems} />
</div>
{!isAdminPage && user.serverAdmin && (
<div className="p-4 border-t">
<Link
href="/admin"
className="flex items-center gap-3 text-muted-foreground hover:text-foreground transition-colors px-3 py-2 rounded-md w-full"
>
<Server className="h-4 w-4" />
Server Admin
</Link>
</div>
)}
</div>
<div className="p-4 space-y-4 border-t shrink-0">
<SupporterStatus />
<OrgSelector orgId={orgId} orgs={orgs} />
<div className="space-y-2">
<div className="text-xs text-muted-foreground text-center">
<Link
href="https://github.com/fosrl/pangolin"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-1"
>
Open Source
<ExternalLink size={12} />
</Link>
</div>
{env?.app?.version && ( {env?.app?.version && (
<div className="text-xs text-muted-foreground text-center"> <div className="text-xs text-muted-foreground text-center">
v{env.app.version} v{env.app.version}
</div> </div>
)} )}
</div> </div>
</SheetContent>
</Sheet>
</div>
)}
{/* Desktop Sidebar */}
{showSidebar && (
<div className="hidden md:flex w-64 border-r bg-card flex-col h-full shrink-0">
{showHeader && (
<div className="flex h-16 items-center border-b px-4 shrink-0">
<Header orgId={orgId} orgs={orgs} />
</div> </div>
</div>
)}
{/* Main content */}
<div
className={cn(
"flex-1 flex flex-col h-full min-w-0",
!showSidebar && "w-full"
)} )}
<div className="flex-1 overflow-y-auto p-4 flex flex-col"> >
<div className="flex-1"> <main className="flex-1 overflow-y-auto p-3 md:p-6 w-full">
<SidebarNav items={navItems} /> <div className="container mx-auto max-w-12xl">
{children}
</div> </div>
{!isAdminPage && user.serverAdmin && ( </main>
<div className="mt-8 pt-4 border-t">
<Link
href="/admin"
className="flex items-center justify-center gap-2 text-sm font-medium text-primary hover:text-primary/80 transition-colors px-4 py-2 rounded-md bg-primary/10 hover:bg-primary/20 w-full"
>
<Server className="h-4 w-4" />
Server Admin
</Link>
</div>
)}
</div>
<div className="p-4 space-y-4 border-t shrink-0">
<SupporterStatus />
<OrgSelector orgId={orgId} orgs={orgs} />
<div className="space-y-2">
<div className="text-xs text-muted-foreground text-center">
<Link
href="https://github.com/fosrl/pangolin"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-1"
>
Open Source
<ExternalLink size={12} />
</Link>
</div>
{env?.app?.version && (
<div className="text-xs text-muted-foreground text-center">
v{env.app.version}
</div>
)}
</div>
</div>
</div> </div>
)}
{/* Main content */}
<div
className={cn(
"flex-1 flex flex-col h-full min-w-0",
!showSidebar && "w-full"
)}
>
{showTopBar && (
<div className="h-16 border-b shrink-0 bg-card">
<div className="flex h-full items-center justify-end px-4">
<TopBar orgId={orgId} orgs={orgs} />
</div>
</div>
)}
{showBreadcrumbs && <Breadcrumbs />}
<main className="flex-1 overflow-y-auto p-3 md:p-6 w-full">
<div className="container mx-auto max-w-12xl">
{children}
</div>
</main>
</div> </div>
</div> </div>
); );

View file

@ -66,7 +66,7 @@ export default function ProfileIcon() {
<Enable2FaForm open={openEnable2fa} setOpen={setOpenEnable2fa} /> <Enable2FaForm open={openEnable2fa} setOpen={setOpenEnable2fa} />
<Disable2FaForm open={openDisable2fa} setOpen={setOpenDisable2fa} /> <Disable2FaForm open={openDisable2fa} setOpen={setOpenDisable2fa} />
<div className="flex items-center md:gap-4 grow min-w-0 gap-2 md:gap-0"> <div className="flex items-center md:gap-2 grow min-w-0 gap-2 md:gap-0">
<span className="truncate max-w-full font-medium min-w-0"> <span className="truncate max-w-full font-medium min-w-0">
{user.email || user.name || user.username} {user.email || user.name || user.username}
</span> </span>

View file

@ -129,22 +129,20 @@ export function SidebarNav({
aria-disabled={disabled} aria-disabled={disabled}
> >
{item.icon && ( {item.icon && (
<span className="mr-3 opacity-70"> <span className="mr-3">{item.icon}</span>
{item.icon}
</span>
)} )}
{item.title} {item.title}
</Link> </Link>
{hasChildren && ( {hasChildren && (
<button <button
onClick={() => toggleItem(hydratedHref)} onClick={() => toggleItem(hydratedHref)}
className="p-2 rounded-md opacity-70 hover:opacity-100" className="p-2 rounded-md text-muted-foreground hover:text-foreground cursor-pointer"
disabled={disabled} disabled={disabled}
> >
{isExpanded ? ( {isExpanded ? (
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-5 w-5" />
) : ( ) : (
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-5 w-5" />
)} )}
</button> </button>
)} )}

View file

@ -1,37 +0,0 @@
"use client";
import ProfileIcon from "@app/components/ProfileIcon";
import Link from "next/link";
interface TopBarProps {
orgId?: string;
orgs?: any;
}
export function TopBar({ orgId, orgs }: TopBarProps) {
return (
<div className="flex items-center justify-end md:justify-between w-full h-full">
<div className="hidden md:flex items-center space-x-4">
<Link
href="https://docs.fossorial.io"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Documentation
</Link>
<Link
href="mailto:support@fossorial.io"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Support
</Link>
</div>
<div>
<ProfileIcon />
</div>
</div>
);
}

View file

@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-99 data-[state=open]:zoom-in-99 data-[state=closed]:slide-out-to-top-[10%] data-[state=open]:slide-in-from-bottom-[10%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-99 data-[state=open]:zoom-in-99 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-bottom-[5%] sm:rounded-lg",
className className
)} )}
{...props} {...props}