config layout

This commit is contained in:
Milo Schwartz 2024-10-13 22:24:02 -04:00
parent 8cd6cbb5ac
commit 55b0838f3a
No known key found for this signature in database
9 changed files with 471 additions and 47 deletions

View file

@ -19,8 +19,10 @@
"@node-rs/argon2": "1.8.3", "@node-rs/argon2": "1.8.3",
"@oslojs/crypto": "1.0.1", "@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0", "@oslojs/encoding": "1.1.0",
"@radix-ui/react-avatar": "1.1.1",
"@radix-ui/react-checkbox": "1.1.2", "@radix-ui/react-checkbox": "1.1.2",
"@radix-ui/react-dialog": "1.1.2", "@radix-ui/react-dialog": "1.1.2",
"@radix-ui/react-dropdown-menu": "2.1.2",
"@radix-ui/react-icons": "1.3.0", "@radix-ui/react-icons": "1.3.0",
"@radix-ui/react-label": "2.1.0", "@radix-ui/react-label": "2.1.0",
"@radix-ui/react-popover": "1.1.2", "@radix-ui/react-popover": "1.1.2",

View file

@ -0,0 +1,80 @@
"use client";
import { Avatar, AvatarFallback } from "@app/components/ui/avatar";
import { Badge } from "@app/components/ui/badge";
import { Button } from "@app/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@app/components/ui/dropdown-menu";
type HeaderProps = {
name?: string;
email: string;
orgName: string;
};
export default function Header({ email, orgName, name }: HeaderProps) {
function getInitials() {
if (name) {
const [firstName, lastName] = name.split(" ");
return `${firstName[0]}${lastName[0]}`;
}
return email.substring(0, 2).toUpperCase();
}
return (
<>
<div className="flex items-center justify-between">
<Badge variant="outline" className="text-md font-bold">{orgName}</Badge>
<div className="flex items-center gap-3">
<span className="text-lg font-medium">{name || email}</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="relative h-10 w-10 rounded-full"
>
<Avatar className="h-10 w-10">
<AvatarFallback>
{getInitials()}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56"
align="end"
forceMount
>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
{name && (
<p className="text-sm font-medium leading-none">
{name}
</p>
)}
<p className="text-xs leading-none text-muted-foreground">
{email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Log out</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</>
);
}

View file

@ -0,0 +1,58 @@
"use client";
import React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
interface TopbarNavProps extends React.HTMLAttributes<HTMLElement> {
items: {
href: string;
title: string;
icon: React.ReactNode;
}[];
disabled?: boolean;
}
export function TopbarNav({
className,
items,
disabled = false,
...props
}: TopbarNavProps) {
const pathname = usePathname();
return (
<nav
className={cn(
"flex overflow-x-auto space-x-4 lg:space-x-6",
disabled && "opacity-50 pointer-events-none",
className,
)}
{...props}
>
{items.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
"px-2 py-3 text-md",
pathname === item.href
? "border-b-2 border-stone-600 text-stone-600"
: "hover:text-gray-600 text-stone-400",
"whitespace-nowrap",
disabled && "cursor-not-allowed",
)}
onClick={disabled ? (e) => e.preventDefault() : undefined}
tabIndex={disabled ? -1 : undefined}
aria-disabled={disabled}
>
<div className="flex items-center gap-2">
{item.icon}
{item.title}
</div>
</Link>
))}
</nav>
);
}

View file

@ -1,64 +1,49 @@
import { Metadata } from "next" import { Metadata } from "next";
import Image from "next/image" import { TopbarNav } from "./components/TopbarNav";
import { LayoutGrid, Tent } from "lucide-react";
import { Separator } from "@/components/ui/separator" import Header from "./components/Header";
import { SidebarNav } from "@/components/sidebar-nav"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Forms", title: "Configuration",
description: "Advanced form example using react-hook-form and Zod.", description: "",
} };
const sidebarNavItems = [ const topNavItems = [
{ {
title: "Sites", title: "Sites",
href: "/configuration/sites", href: "/configuration/sites",
icon: <Tent />,
}, },
{ {
title: "Resources", title: "Resources",
href: "/configuration/resources", href: "/configuration/resources",
icon: <LayoutGrid />,
}, },
] ];
interface SettingsLayoutProps { interface ConfigurationLaytoutProps {
children: React.ReactNode, children: React.ReactNode;
params: { siteId: string } params: { siteId: string };
} }
export default function SettingsLayout({ children, params }: SettingsLayoutProps) { export default async function ConfigurationLaytout({
children,
params,
}: ConfigurationLaytoutProps) {
return ( return (
<> <>
<div className="md:hidden"> <div className="w-full bg-stone-200 border-b border-stone-300 mb-5 select-none px-3">
<Image <div className="container mx-auto flex flex-col content-between gap-3 pt-2">
src="/configuration/forms-light.png" <Header
width={1280} email="mschwartz10612@gmail.com"
height={791} orgName="Home Lab 1"
alt="Forms" name="Milo Schwartz"
className="block dark:hidden" />
/> <TopbarNav items={topNavItems} />
<Image
src="/configuration/forms-dark.png"
width={1280}
height={791}
alt="Forms"
className="hidden dark:block"
/>
</div>
<div className="hidden space-y-6 p-10 pb-16 md:block">
<div className="space-y-0.5">
<h2 className="text-2xl font-bold tracking-tight">Settings</h2>
<p className="text-muted-foreground">
{ params.siteId == "create" ? "Create site..." : "Manage settings on site " + params.siteId }.
</p>
</div>
<Separator className="my-6" />
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<aside className="-mx-4 lg:w-1/5">
<SidebarNav items={sidebarNavItems.map(i => { i.href = i.href.replace("{siteId}", params.siteId); return i})} disabled={params.siteId == "create"} />
</aside>
<div className="flex-1 lg:max-w-2xl">{children}</div>
</div> </div>
</div> </div>
<div className="container mx-auto px-3">{children}</div>
</> </>
) );
} }

View file

@ -1,7 +1,14 @@
export default async function Page() { export default async function Page() {
return ( return (
<> <>
<p>This is where the table goes...</p> <div className="space-y-0.5 select-none">
<h2 className="text-2xl font-bold tracking-tight">
Manage Resources
</h2>
<p className="text-muted-foreground">
Create secure proxies to your private resources.
</p>
</div>
</> </>
); );
} }

View file

@ -3,8 +3,14 @@ import Link from "next/link";
export default async function Page() { export default async function Page() {
return ( return (
<> <>
<p>This is where the table goes...</p> <div className="space-y-0.5 select-none">
<Link href="/configuration/sites/123">Open up the site 123</Link> <h2 className="text-2xl font-bold tracking-tight">
Manage Sites
</h2>
<p className="text-muted-foreground">
Manage your existing sites here or create a new one.
</p>
</div>
</> </>
); );
} }

View file

@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View file

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View file

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}