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