feat: initial commit

This commit is contained in:
Mauricio Siu
2024-04-28 23:57:52 -06:00
parent 8857a20344
commit be56ba046c
412 changed files with 60777 additions and 1 deletions

View File

@@ -0,0 +1,29 @@
import { Navbar } from "./navbar";
import { NavigationTabs, type TabState } from "./navigation-tabs";
interface Props {
children: React.ReactNode;
tab: TabState;
}
export const DashboardLayout = ({ children, tab }: Props) => {
return (
<div>
<div
className="bg-radial relative flex flex-col bg-background pt-6"
id="app-container"
>
<div className="flex items-center justify-center">
<div className="w-full">
<Navbar />
<main className="mt-6 flex w-full flex-col items-center">
<div className="w-full max-w-8xl px-4 lg:px-8">
<NavigationTabs tab={tab}>{children}</NavigationTabs>
</div>
</main>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,137 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { Logo } from "../shared/logo";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { Badge } from "../ui/badge";
import { useRouter } from "next/router";
import { api } from "@/utils/api";
export const Navbar = () => {
const router = useRouter();
const { data } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: data?.id || "",
},
{
enabled: !!data?.id && data?.rol === "user",
},
);
const { mutateAsync } = api.auth.logout.useMutation();
return (
<nav className="border-divider sticky inset-x-0 top-0 z-40 flex h-auto w-full items-center justify-center border-b bg-background/70 backdrop-blur-lg backdrop-saturate-150 data-[menu-open=true]:border-none data-[menu-open=true]:backdrop-blur-xl">
<header className="relative z-40 flex h-[var(--navbar-height)] w-full max-w-8xl flex-row flex-nowrap items-center justify-between gap-4 px-4 sm:px-6">
<div className="text-medium box-border flex flex-grow basis-0 flex-row flex-nowrap items-center justify-start whitespace-nowrap bg-transparent no-underline">
<Link
href="/dashboard/projects"
className={cn("flex flex-row items-center gap-2")}
>
<Logo />
<span className="text-sm font-semibold text-primary">Dokploy</span>
</Link>
</div>
<ul
className="ml-auto flex h-12 max-w-fit flex-row flex-nowrap items-center gap-0 data-[justify=end]:flex-grow data-[justify=start]:flex-grow data-[justify=end]:basis-0 data-[justify=start]:basis-0 data-[justify=start]:justify-start data-[justify=end]:justify-end data-[justify=center]:justify-center"
data-justify="end"
>
<li className="text-medium mr-2 box-border hidden list-none whitespace-nowrap data-[active=true]:font-semibold data-[active=true]:text-primary lg:flex">
{/* <Badge>PRO</Badge> */}
</li>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className="size-10 cursor-pointer border border-border items-center">
<AvatarImage src={data?.image || ""} alt="@shadcn" />
<AvatarFallback>
{data?.email
?.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end">
<DropdownMenuLabel className="flex flex-col">
My Account
<span className="text-xs font-normal text-muted-foreground">
{data?.email}
</span>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/projects");
}}
>
Projects
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/monitoring");
}}
>
Monitoring
</DropdownMenuItem>
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/traefik");
}}
>
Traefik
</DropdownMenuItem>
)}
{(data?.rol === "admin" || user?.canAccessToDocker) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/docker", undefined, {
shallow: true,
});
}}
>
Docker
</DropdownMenuItem>
)}
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings/server");
}}
>
Settings
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer"
onClick={async () => {
await mutateAsync().then(() => {
router.push("/");
});
}}
>
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ul>
</header>
</nav>
);
};

View File

@@ -0,0 +1,121 @@
import { AddProject } from "@/components/dashboard/projects/add";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { api } from "@/utils/api";
export type TabState =
| "projects"
| "monitoring"
| "settings"
| "traefik"
| "docker";
interface Props {
tab: TabState;
children: React.ReactNode;
}
export const NavigationTabs = ({ tab, children }: Props) => {
const router = useRouter();
const { data } = api.auth.get.useQuery();
const [activeTab, setActiveTab] = useState<TabState>(tab);
const { data: user } = api.user.byAuthId.useQuery(
{
authId: data?.id || "",
},
{
enabled: !!data?.id && data?.rol === "user",
},
);
useEffect(() => {
setActiveTab(tab);
}, [tab]);
return (
<div className="gap-12 min-h-screen">
<header className="mb-6 flex w-full items-center gap-2 justify-between flex-wrap">
<div className="flex flex-col gap-2">
<h1 className="text-xl font-bold lg:text-3xl">
{tab === "projects" && "Projects"}
{tab === "monitoring" && "Monitoring"}
{tab === "settings" && "Settings"}
{tab === "traefik" && "Traefik"}
{tab === "docker" && "Docker"}
</h1>
<p className="lg:text-medium text-muted-foreground">
{tab === "projects" && "Manage your deployments"}
{tab === "monitoring" && "Watch the usage of your server"}
{tab === "settings" && "Check the configuration"}
{tab === "traefik" && "Read the traefik config and update it"}
{tab === "docker" && "Manage the docker containers"}
</p>
</div>
{tab === "projects" &&
(data?.rol === "admin" || user?.canCreateProjects) && <AddProject />}
</header>
<div className="flex w-full justify-between gap-8 ">
<Tabs
value={activeTab}
className="w-full"
onValueChange={(e) => {
if (e === "settings") {
router.push("/dashboard/settings/server");
} else {
router.push(`/dashboard/${e}`);
}
setActiveTab(e as TabState);
}}
>
{/* className="grid w-fit grid-cols-4 bg-transparent" */}
<div className="flex flex-row items-center justify-between w-full gap-4 max-sm:overflow-x-auto">
<TabsList className="md:grid md:w-fit md:grid-cols-5 justify-start bg-transparent">
<TabsTrigger
value="projects"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Projects
</TabsTrigger>
<TabsTrigger
value="monitoring"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Monitoring
</TabsTrigger>
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
<TabsTrigger
value="traefik"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Traefik File System
</TabsTrigger>
)}
{(data?.rol === "admin" || user?.canAccessToDocker) && (
<TabsTrigger
value="docker"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Docker
</TabsTrigger>
)}
<TabsTrigger
value="settings"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Settings
</TabsTrigger>
</TabsList>
</div>
<TabsContent value={activeTab} className="w-full">
{children}
</TabsContent>
</Tabs>
</div>
</div>
);
};

View File

@@ -0,0 +1,8 @@
import type React from "react";
interface Props {
children: React.ReactNode;
}
export const OnboardingLayout = ({ children }: Props) => {
return <>{children}</>;
};

View File

@@ -0,0 +1,25 @@
import { Navbar } from "./navbar";
interface Props {
children: React.ReactNode;
}
export const ProjectLayout = ({ children }: Props) => {
return (
<div>
<div
className="bg-radial relative flex flex-col bg-background pt-6"
id="app-container"
>
<div className="flex items-center justify-center">
<div className="w-full">
<Navbar />
<main className="mt-6 flex w-full flex-col items-center">
<div className="w-full max-w-8xl px-4 lg:px-8">{children}</div>
</main>
</div>
</div>
</div>
</div>
);
};

View File

View File

@@ -0,0 +1,241 @@
interface Props {
children: React.ReactNode;
}
export const SettingsLayout = ({ children }: Props) => {
const { data } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: data?.id || "",
},
{
enabled: !!data?.id && data?.rol === "user",
},
);
return (
<div className="flex flex-row gap-4 my-8 w-full flex-wrap md:flex-nowrap">
<div className="md:max-w-[18rem] w-full">
<Nav
links={[
...(data?.rol === "admin"
? [
{
title: "Server",
icon: Activity,
href: "/dashboard/settings/server",
},
]
: []),
{
title: "Profile",
icon: User2,
href: "/dashboard/settings/profile",
},
{
title: "Appareance",
label: "",
icon: Route,
href: "/dashboard/settings/appearance",
},
...(data?.rol === "admin"
? [
{
title: "S3 Destinations",
label: "",
icon: Database,
href: "/dashboard/settings/destinations",
},
{
title: "Certificates",
label: "",
icon: ShieldCheck,
href: "/dashboard/settings/certificates",
},
{
title: "Users",
label: "",
icon: Users,
href: "/dashboard/settings/users",
},
]
: []),
]}
/>
</div>
{children}
</div>
);
};
import Link from "next/link";
import {
Activity,
Database,
Route,
ShieldCheck,
User2,
Users,
type LucideIcon,
} from "lucide-react";
import { buttonVariants } from "@/components/ui/button";
import { useRouter } from "next/router";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
interface NavProps {
links: {
title: string;
label?: string;
icon: LucideIcon;
href: string;
}[];
}
export const Nav = ({ links }: NavProps) => {
const router = useRouter();
return (
<div className="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2 ">
<nav className="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
{links.map((link, index) => {
const isActive = router.pathname === link.href;
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
return (
<Link
key={index}
href={link.href}
className={cn(
buttonVariants({ variant: "ghost", size: "sm" }),
isActive &&
"dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white",
"justify-start",
)}
>
<link.icon className="mr-2 h-4 w-4" />
{link.title}
{link.label && (
<span
className={cn(
"ml-auto",
isActive && "text-background dark:text-white",
)}
>
{link.label}
</span>
)}
</Link>
);
})}
{/* {!isCollapsed ? (
<Accordion collapsible type="single" className="">
<AccordionItem value="follow-up" className="">
<AccordionTrigger
className={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"hover:no-underline py-0 text-start justify-start flex items-center gap-2 px-3 mb-2",
)}
>
<div className="flex flex-row items-center gap-2 justify-between w-full">
<div className="flex flex-row gap-2 items-center">
<Settings className="h-4 w-4" />
<span className=" dark:hover:text-white">Settings</span>
</div>
</div>
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionTrigger>
<AccordionContent className="ml-9">
<Link
className={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"hover:no-underline w-full text-start justify-start px-2 gap-2",
)}
href="/dashboard/settings"
>
<User2 className="h-4 w-4" />
Account
</Link>
<Link
className={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"hover:no-underline w-full text-start justify-start px-2 gap-2",
)}
href="/dashboard/server"
>
<Computer className="h-4 w-4" />
Server
</Link>
<Link
className={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"hover:no-underline w-full text-start justify-start px-2 gap-2",
)}
href="/dashboard/users"
>
<Users className="h-4 w-4" />
Users
</Link>
</AccordionContent>
</AccordionItem>
</Accordion>
) : (
<>
{[
{
title: "Account",
icon: User2,
label: "",
href: "/dashboard/server",
},
{
title: "Server",
icon: Computer,
label: "",
href: "/dashboard/users",
},
{
title: "Users",
icon: Users,
label: "",
href: "/dashboard/traefik",
},
].map((link, index) => {
const isActive = router.pathname === link.href;
return (
<Tooltip key={index} delayDuration={0}>
<TooltipTrigger asChild>
<Link
href={link.href}
className={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"h-9 w-9",
isActive &&
"dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white",
)}
>
<link.icon className="h-4 w-4" />
<span className="sr-only">{link.title}</span>
</Link>
</TooltipTrigger>
<TooltipContent
side="right"
className="flex items-center gap-4"
>
{link.title}
{link.label && (
<span className="ml-auto text-muted-foreground">
{link.label}
</span>
)}
</TooltipContent>
</Tooltip>
);
})}
</>
)} */}
</nav>
</div>
);
};