diff --git a/package.json b/package.json index 72ef147..f934686 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "ipaddr.js": "^2.1.0", "lucide-react": "^0.298.0", "next": "14.0.4", + "next-nprogress-bar": "^2.1.2", "next-themes": "^0.2.1", "node-os-utils": "^1.3.7", "react": "18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef20f55..4ff452a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ dependencies: next: specifier: 14.0.4 version: 14.0.4(react-dom@18.2.0)(react@18.2.0) + next-nprogress-bar: + specifier: ^2.1.2 + version: 2.1.2 next-themes: specifier: ^0.2.1 version: 0.2.1(next@14.0.4)(react-dom@18.2.0)(react@18.2.0) @@ -6047,6 +6050,12 @@ packages: engines: {node: '>= 0.6'} dev: false + /next-nprogress-bar@2.1.2: + resolution: {integrity: sha512-2Df5d7fr6uPx+BX8MkoWCfl+RjG+uWI5mA399e5sEe8mbT3q/GIUvCXLzBgJBIISpKuMmdLAOYEzqpjlsRVOWw==} + dependencies: + nprogress: 0.2.0 + dev: false + /next-themes@0.2.1(next@14.0.4)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: @@ -6227,6 +6236,10 @@ packages: set-blocking: 2.0.0 dev: false + /nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} diff --git a/src/app/(dashboard)/project/[id]/_context/ProjectContext.tsx b/src/app/(dashboard)/project/[id]/_context/ProjectContext.tsx index fe35b83..8abad7a 100644 --- a/src/app/(dashboard)/project/[id]/_context/ProjectContext.tsx +++ b/src/app/(dashboard)/project/[id]/_context/ProjectContext.tsx @@ -6,7 +6,17 @@ import { type RouterOutputs } from "~/trpc/shared"; export type BasicServiceDetails = RouterOutputs["projects"]["get"]["services"][number]; export type ProjectContextType = RouterOutputs["projects"]["get"] & { + /** + * Base project path + * Example: `/project/123` + */ path: string; + + /** + * Base service path + * Example: `/project/123/service/456` + */ + servicePath: string; selectedService?: BasicServiceDetails; }; diff --git a/src/app/(dashboard)/project/[id]/service/[serviceId]/home/page.tsx b/src/app/(dashboard)/project/[id]/service/[serviceId]/home/page.tsx new file mode 100644 index 0000000..b02fe32 --- /dev/null +++ b/src/app/(dashboard)/project/[id]/service/[serviceId]/home/page.tsx @@ -0,0 +1,13 @@ +import { DeleteButton } from "../_components/DeleteButton"; + +export default function ServicePage({ + params: { serviceId }, +}: { + params: { serviceId: string }; +}) { + return ( +
+ Hello world from {serviceId} +
+ ); +} diff --git a/src/app/(dashboard)/project/[id]/service/[serviceId]/layout.tsx b/src/app/(dashboard)/project/[id]/service/[serviceId]/layout.tsx new file mode 100644 index 0000000..71ae5e6 --- /dev/null +++ b/src/app/(dashboard)/project/[id]/service/[serviceId]/layout.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { BoxesIcon, CloudyIcon, CodeIcon, HomeIcon } from "lucide-react"; +import { SidebarNav, type SidebarNavProps } from "~/components/SidebarNav"; +import { useProject } from "../../_context/ProjectContext"; + +const sidebarNavItems = [ + { + title: "Home", + description: "Quick overview of all containers for this project.", + href: "/home", + icon: HomeIcon, + }, + + { + type: "divider", + title: "Deployment", + }, + + { + title: "Containers", + description: "Lists all containers deployed for this service.", + href: "/containers", + icon: BoxesIcon, + }, + { + title: "Deployments", + description: "All deployments for this service.", + href: "/deployments", + icon: CloudyIcon, + }, + { + title: "Source", + description: "Source settings", + href: "/source", + icon: CodeIcon, + }, +] as const; + +export default function ProjectHomeLayout({ + children, +}: { + children: React.ReactNode; +}) { + const project = useProject(); + const items = sidebarNavItems.map((item) => ({ + ...item, + href: "href" in item ? `${project.path}${item.href}` : undefined, + })) as SidebarNavProps["items"]; + + return ( +
+
+ +
+ {/* */} + {/* */} + {children} +
+
+
+ ); +} diff --git a/src/app/(dashboard)/project/[id]/service/[serviceId]/page.tsx b/src/app/(dashboard)/project/[id]/service/[serviceId]/page.tsx index 7766251..88d3338 100644 --- a/src/app/(dashboard)/project/[id]/service/[serviceId]/page.tsx +++ b/src/app/(dashboard)/project/[id]/service/[serviceId]/page.tsx @@ -1,13 +1,13 @@ -import { DeleteButton } from "./_components/DeleteButton"; +"use client"; -export default function ServicePage({ - params: { serviceId }, -}: { - params: { serviceId: string }; -}) { - return ( -
- Hello world from {serviceId} -
- ); +import { usePathname, useRouter } from "next/navigation"; + +export default function ServicePage() { + const router = useRouter(); + const pathname = usePathname(); + + // redirect to ./home + router.push(pathname + "/home"); + + return
Redirecting you...
; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3fc848a..7d51d1e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,7 @@ import { cookies } from "next/headers"; import { ThemeProvider } from "~/components/contexts/ThemeProvider"; import { ToastProvider } from "~/components/contexts/ToastProvider"; import { TRPCReactProvider } from "~/trpc/react"; +import { AppProgressBar } from "./providers"; const outfit = Outfit({ subsets: ["latin"], @@ -28,7 +29,14 @@ export default function RootLayout({ - {children} + + + {children} + diff --git a/src/app/providers.tsx b/src/app/providers.tsx new file mode 100644 index 0000000..d851136 --- /dev/null +++ b/src/app/providers.tsx @@ -0,0 +1,4 @@ +"use client"; + +// reexport AppProgressBar as the original source is missing a "use client" directive +export { AppProgressBar } from "next-nprogress-bar"; diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 97736a2..e34416c 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -1,15 +1,27 @@ "use client"; +import { type LucideIcon } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import React from "react"; import { buttonVariants } from "~/components/ui/button"; import { cn } from "~/utils/utils"; -interface SidebarNavProps extends React.HTMLAttributes { - items: { - href: string; - title: string; - }[]; +type SidebarNavEntry = { + type?: "entry"; + href: string; + title: string; + icon?: LucideIcon; +}; + +type SidebarNavDivider = { + type: "divider"; + title: string; + icon?: LucideIcon; +}; + +export interface SidebarNavProps extends React.HTMLAttributes { + items: (SidebarNavEntry | SidebarNavDivider)[]; } export function SidebarNav({ className, items, ...props }: SidebarNavProps) { @@ -23,21 +35,35 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) { )} {...props} > - {items.map((item) => ( - - {item.title} - - ))} + {items.map((item, i) => + item.type === "divider" ? ( +

+ {item.title} +

+ ) : ( + + {item.icon && ( +
+ +
+ )} + {item.title} + + ), + )} ); }