Milo Schwartz пре 9 месеци
родитељ
комит
55b0838f3a

+ 2 - 0
package.json

@@ -19,8 +19,10 @@
         "@node-rs/argon2": "1.8.3",
         "@oslojs/crypto": "1.0.1",
         "@oslojs/encoding": "1.1.0",
+        "@radix-ui/react-avatar": "1.1.1",
         "@radix-ui/react-checkbox": "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-label": "2.1.0",
         "@radix-ui/react-popover": "1.1.2",

+ 80 - 0
src/app/configuration/components/Header.tsx

@@ -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>
+        </>
+    );
+}

+ 58 - 0
src/app/configuration/components/TopbarNav.tsx

@@ -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>
+    );
+}

+ 29 - 44
src/app/configuration/layout.tsx

@@ -1,64 +1,49 @@
-import { Metadata } from "next"
-import Image from "next/image"
-
-import { Separator } from "@/components/ui/separator"
-import { SidebarNav } from "@/components/sidebar-nav"
+import { Metadata } from "next";
+import { TopbarNav } from "./components/TopbarNav";
+import { LayoutGrid, Tent } from "lucide-react";
+import Header from "./components/Header";
 
 export const metadata: Metadata = {
-    title: "Forms",
-    description: "Advanced form example using react-hook-form and Zod.",
-}
+    title: "Configuration",
+    description: "",
+};
 
-const sidebarNavItems = [
+const topNavItems = [
     {
         title: "Sites",
         href: "/configuration/sites",
+        icon: <Tent />,
     },
     {
         title: "Resources",
         href: "/configuration/resources",
+        icon: <LayoutGrid />,
     },
-]
+];
 
-interface SettingsLayoutProps {
-    children: React.ReactNode,
-    params: { siteId: string }
+interface ConfigurationLaytoutProps {
+    children: React.ReactNode;
+    params: { siteId: string };
 }
 
-export default function SettingsLayout({ children, params }: SettingsLayoutProps) {
+export default async function ConfigurationLaytout({
+    children,
+    params,
+}: ConfigurationLaytoutProps) {
     return (
         <>
-            <div className="md:hidden">
-                <Image
-                    src="/configuration/forms-light.png"
-                    width={1280}
-                    height={791}
-                    alt="Forms"
-                    className="block dark:hidden"
-                />
-                <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 className="w-full bg-stone-200 border-b border-stone-300 mb-5 select-none px-3">
+                <div className="container mx-auto flex flex-col content-between gap-3 pt-2">
+                    <Header
+                        email="mschwartz10612@gmail.com"
+                        orgName="Home Lab 1"
+                        name="Milo Schwartz"
+                    />
+                    <TopbarNav items={topNavItems} />
                 </div>
             </div>
+
+            <div className="container mx-auto px-3">{children}</div>
         </>
-    )
+    );
 }

+ 8 - 1
src/app/configuration/resources/page.tsx

@@ -1,7 +1,14 @@
 export default async function Page() {
     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>
         </>
     );
 }

+ 8 - 2
src/app/configuration/sites/page.tsx

@@ -3,8 +3,14 @@ import Link from "next/link";
 export default async function Page() {
     return (
         <>
-           <p>This is where the table goes...</p>
-            <Link href="/configuration/sites/123">Open up the site 123</Link>
+            <div className="space-y-0.5 select-none">
+                <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>
         </>
     );
 }

+ 50 - 0
src/components/ui/avatar.tsx

@@ -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 }

+ 36 - 0
src/components/ui/badge.tsx

@@ -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 }

+ 200 - 0
src/components/ui/dropdown-menu.tsx

@@ -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,
+}