Implement initial application structure and UI.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/54918520-d42c-44cd-b4f4-9e5a42eb5be1.jpg
This commit is contained in:
alphaeusmote
2025-04-09 21:34:26 +00:00
parent e1340a29cb
commit cba930e4d3
99 changed files with 21903 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
import { Menu, User } from "lucide-react";
import { ThemeToggle } from "@/components/shared/theme-toggle";
import { useAuth } from "@/hooks/use-auth";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
interface HeaderProps {
title?: string;
onMobileMenuOpen: () => void;
}
export function Header({ title, onMobileMenuOpen }: HeaderProps) {
const { user, logoutMutation } = useAuth();
const getInitials = (name: string) => {
return name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase();
};
return (
<header className="lg:hidden bg-card border-b border-border p-4 flex items-center justify-between">
<div className="flex items-center">
<button
id="mobile-menu-button"
className="mr-2 p-2 rounded-md hover:bg-accent"
onClick={onMobileMenuOpen}
aria-label="Open menu"
>
<Menu className="w-6 h-6" />
</button>
<div className="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="w-6 h-6 text-primary mr-2"
>
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
</svg>
<span className="font-semibold text-xl">
{title ? title : "DynamiDNS"}
</span>
</div>
</div>
<div className="flex items-center space-x-2">
<ThemeToggle />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<AvatarFallback>
{user?.fullName
? getInitials(user.fullName)
: user?.username.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="cursor-pointer"
onClick={() => logoutMutation.mutate()}
disabled={logoutMutation.isPending}
>
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
);
}
@@ -0,0 +1,48 @@
import { ReactNode, useState } from "react";
import { Sidebar } from "./sidebar";
import { Header } from "./header";
import { MobileSidebar } from "./mobile-sidebar";
interface MainLayoutProps {
children: ReactNode;
title?: string;
description?: string;
}
export function MainLayout({ children, title, description }: MainLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className="min-h-screen flex flex-col">
{/* Mobile Header */}
<Header
title={title}
onMobileMenuOpen={() => setSidebarOpen(true)}
/>
<div className="flex flex-1">
{/* Sidebar Navigation */}
<Sidebar />
{/* Mobile Sidebar */}
<MobileSidebar
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
/>
{/* Main Content */}
<main className="flex-1 overflow-y-auto bg-background">
<div className="container mx-auto px-4 py-6">
{(title || description) && (
<div className="mb-8">
{title && <h1 className="text-2xl font-semibold mb-2">{title}</h1>}
{description && <p className="text-muted-foreground">{description}</p>}
</div>
)}
{children}
</div>
</main>
</div>
</div>
);
}
@@ -0,0 +1,188 @@
import { Link, useLocation } from "wouter";
import { cn } from "@/lib/utils";
import { OrganizationSelector } from "@/components/shared/organization-selector";
import { useAuth } from "@/hooks/use-auth";
import {
LayoutDashboard,
Home,
Globe,
BarChart2,
FileEdit,
Key,
Users,
Settings,
CreditCard,
X,
LogOut,
} from "lucide-react";
import { Button } from "@/components/ui/button";
interface MobileSidebarProps {
isOpen: boolean;
onClose: () => void;
}
export function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
const [location] = useLocation();
const { user, logoutMutation } = useAuth();
// Define navigation items
const navItems = [
{
title: "Dashboard",
href: "/",
icon: <LayoutDashboard className="w-5 h-5 mr-3" />,
},
{
title: "Domains",
href: "/domains",
icon: <Home className="w-5 h-5 mr-3" />,
},
{
title: "DNS Records",
href: "/dns-records",
icon: <Globe className="w-5 h-5 mr-3" />,
},
{
title: "Metrics",
href: "/metrics",
icon: <BarChart2 className="w-5 h-5 mr-3" />,
},
{
title: "History",
href: "/history",
icon: <FileEdit className="w-5 h-5 mr-3" />,
},
{
title: "API Tokens",
href: "/api-tokens",
icon: <Key className="w-5 h-5 mr-3" />,
},
];
// Define admin navigation items
const adminNavItems = [
{
title: "Users & Roles",
href: "/users-roles",
icon: <Users className="w-5 h-5 mr-3" />,
roles: ["admin"],
},
{
title: "Providers",
href: "/providers",
icon: <CreditCard className="w-5 h-5 mr-3" />,
roles: ["admin", "manager"],
},
{
title: "Settings",
href: "/settings",
icon: <Settings className="w-5 h-5 mr-3" />,
roles: ["admin", "manager"],
},
];
// Filter admin items based on user role
const filteredAdminItems = adminNavItems.filter(
item => !item.roles || (user && item.roles.includes(user.role))
);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-40 lg:hidden">
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm" onClick={onClose}></div>
<div className="fixed inset-y-0 left-0 w-full max-w-xs bg-card shadow-lg">
<div className="flex h-full flex-col">
<div className="p-4 flex items-center justify-between border-b border-border">
<div className="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="w-6 h-6 text-primary mr-2"
>
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
</svg>
<span className="font-semibold text-xl">DynamiDNS</span>
</div>
<button
onClick={onClose}
className="p-2 rounded-md hover:bg-accent"
aria-label="Close sidebar"
>
<X size={24} />
</button>
</div>
{/* Organization Selector */}
<div className="px-4 py-2">
<OrganizationSelector />
</div>
<nav className="flex-1 py-4 px-2 space-y-1 overflow-y-auto">
{navItems.map((item) => (
<Link key={item.href} href={item.href}>
<a
className={cn(
"flex items-center px-3 py-2 text-sm font-medium rounded-md",
location === item.href
? "bg-primary text-primary-foreground"
: "hover:bg-accent hover:text-accent-foreground"
)}
onClick={onClose}
>
{item.icon}
{item.title}
</a>
</Link>
))}
{filteredAdminItems.length > 0 && (
<div className="pt-4">
<div className="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Administration
</div>
{filteredAdminItems.map((item) => (
<Link key={item.href} href={item.href}>
<a
className={cn(
"mt-1 flex items-center px-3 py-2 text-sm font-medium rounded-md",
location === item.href
? "bg-primary text-primary-foreground"
: "hover:bg-accent hover:text-accent-foreground"
)}
onClick={onClose}
>
{item.icon}
{item.title}
</a>
</Link>
))}
</div>
)}
</nav>
<div className="p-4 border-t border-border">
<Button
variant="outline"
className="w-full justify-start"
onClick={() => {
logoutMutation.mutate();
onClose();
}}
disabled={logoutMutation.isPending}
>
<LogOut className="mr-2 h-4 w-4" />
Log out
</Button>
</div>
</div>
</div>
</div>
);
}
+165
View File
@@ -0,0 +1,165 @@
import { Link, useLocation } from "wouter";
import { cn } from "@/lib/utils";
import { ThemeToggle } from "@/components/shared/theme-toggle";
import { OrganizationSelector } from "@/components/shared/organization-selector";
import { useAuth } from "@/hooks/use-auth";
import {
LayoutDashboard,
Home,
Globe,
BarChart2,
FileEdit,
Key,
Users,
Settings,
CreditCard,
LogOut,
} from "lucide-react";
import { Button } from "@/components/ui/button";
export function Sidebar() {
const [location] = useLocation();
const { user, logoutMutation } = useAuth();
// Define navigation items
const navItems = [
{
title: "Dashboard",
href: "/",
icon: <LayoutDashboard className="w-5 h-5 mr-3" />,
},
{
title: "Domains",
href: "/domains",
icon: <Home className="w-5 h-5 mr-3" />,
},
{
title: "DNS Records",
href: "/dns-records",
icon: <Globe className="w-5 h-5 mr-3" />,
},
{
title: "Metrics",
href: "/metrics",
icon: <BarChart2 className="w-5 h-5 mr-3" />,
},
{
title: "History",
href: "/history",
icon: <FileEdit className="w-5 h-5 mr-3" />,
},
{
title: "API Tokens",
href: "/api-tokens",
icon: <Key className="w-5 h-5 mr-3" />,
},
];
// Define admin navigation items
const adminNavItems = [
{
title: "Users & Roles",
href: "/users-roles",
icon: <Users className="w-5 h-5 mr-3" />,
roles: ["admin"],
},
{
title: "Providers",
href: "/providers",
icon: <CreditCard className="w-5 h-5 mr-3" />,
roles: ["admin", "manager"],
},
{
title: "Settings",
href: "/settings",
icon: <Settings className="w-5 h-5 mr-3" />,
roles: ["admin", "manager"],
},
];
// Filter admin items based on user role
const filteredAdminItems = adminNavItems.filter(
item => !item.roles || (user && item.roles.includes(user.role))
);
return (
<aside className="hidden lg:flex lg:flex-col w-64 border-r border-border bg-card">
<div className="p-4 flex items-center justify-between">
<div className="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="w-6 h-6 text-primary mr-2"
>
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
</svg>
<span className="font-semibold text-xl">DynamiDNS</span>
</div>
</div>
{/* Organization Selector */}
<div className="px-4 py-2">
<OrganizationSelector />
</div>
<nav className="flex-1 py-4 px-2 space-y-1 overflow-y-auto">
{navItems.map((item) => (
<Link key={item.href} href={item.href}>
<a
className={cn(
"flex items-center px-3 py-2 text-sm font-medium rounded-md",
location === item.href
? "bg-primary text-primary-foreground"
: "hover:bg-accent hover:text-accent-foreground"
)}
>
{item.icon}
{item.title}
</a>
</Link>
))}
{filteredAdminItems.length > 0 && (
<div className="pt-4">
<div className="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Administration
</div>
{filteredAdminItems.map((item) => (
<Link key={item.href} href={item.href}>
<a
className={cn(
"mt-1 flex items-center px-3 py-2 text-sm font-medium rounded-md",
location === item.href
? "bg-primary text-primary-foreground"
: "hover:bg-accent hover:text-accent-foreground"
)}
>
{item.icon}
{item.title}
</a>
</Link>
))}
</div>
)}
</nav>
<div className="p-4 border-t border-border flex flex-col gap-2">
<ThemeToggle />
<Button
variant="outline"
className="w-full justify-start"
onClick={() => logoutMutation.mutate()}
disabled={logoutMutation.isPending}
>
<LogOut className="mr-2 h-4 w-4" />
Log out
</Button>
</div>
</aside>
);
}