diff --git a/apps/dashboard/src/@/actions/joinWaitlist.ts b/apps/dashboard/src/@/actions/joinWaitlist.ts
new file mode 100644
index 00000000000..58f324b8481
--- /dev/null
+++ b/apps/dashboard/src/@/actions/joinWaitlist.ts
@@ -0,0 +1,34 @@
+"use server";
+
+import { getAuthToken } from "../../app/api/lib/getAuthToken";
+import { API_SERVER_URL } from "../constants/env";
+
+export async function joinTeamWaitlist(options: {
+ teamSlug: string;
+ // currently only 'nebula' is supported
+ scope: "nebula";
+}) {
+ const { teamSlug, scope } = options;
+ const token = await getAuthToken();
+
+ if (!token) {
+ throw new Error("No Auth token");
+ }
+
+ const res = await fetch(`${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({
+ scope,
+ }),
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to join waitlist");
+ }
+
+ return true;
+}
diff --git a/apps/dashboard/src/@/api/team.ts b/apps/dashboard/src/@/api/team.ts
index 6900dd8a77f..e60af9f4615 100644
--- a/apps/dashboard/src/@/api/team.ts
+++ b/apps/dashboard/src/@/api/team.ts
@@ -2,6 +2,7 @@ import "server-only";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
+import { getAuthToken } from "../../app/api/lib/getAuthToken";
export type Team = {
id: string;
@@ -18,11 +19,7 @@ export type Team = {
};
export async function getTeamBySlug(slug: string) {
- const cookiesManager = await cookies();
- const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
- const token = activeAccount
- ? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
- : null;
+ const token = await getAuthToken();
if (!token) {
return null;
@@ -60,3 +57,31 @@ export async function getTeams() {
}
return [];
}
+
+type TeamNebulWaitList = {
+ onWaitlist: boolean;
+ createdAt: null | string;
+};
+
+export async function getTeamNebulaWaitList(teamSlug: string) {
+ const token = await getAuthToken();
+
+ if (!token) {
+ return null;
+ }
+
+ const res = await fetch(
+ `${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist?scope=nebula`,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ );
+
+ if (res.ok) {
+ return (await res.json()).result as TeamNebulWaitList;
+ }
+
+ return null;
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx
index 9dc1f2bc76e..da83b409fe3 100644
--- a/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx
@@ -55,6 +55,10 @@ export default async function TeamLayout(props: {
path: `/team/${params.team_slug}/~/ecosystem`,
name: "Ecosystems",
},
+ {
+ path: `/team/${params.team_slug}/~/nebula`,
+ name: "Nebula",
+ },
{
path: `/team/${params.team_slug}/~/usage`,
name: "Usage",
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/nebula/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/nebula/page.tsx
new file mode 100644
index 00000000000..175b8c33ee7
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/nebula/page.tsx
@@ -0,0 +1,38 @@
+import { getTeamBySlug, getTeamNebulaWaitList } from "@/api/team";
+import { redirect } from "next/navigation";
+import { JoinNebulaWaitlistPage } from "../../../[project_slug]/nebula/components/nebula-waitlist-page.client";
+
+export default async function Page(props: {
+ params: Promise<{
+ team_slug: string;
+ }>;
+}) {
+ const params = await props.params;
+ const team = await getTeamBySlug(params.team_slug);
+
+ if (!team) {
+ redirect(
+ `/login?next=${encodeURIComponent(`/team/${params.team_slug}/~/nebula`)}`,
+ );
+ }
+
+ const nebulaWaitList = await getTeamNebulaWaitList(team.slug);
+
+ // this should never happen
+ if (!nebulaWaitList) {
+ return (
+
+
+ Something went wrong trying to fetch the nebula waitlist
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/layout.tsx
index 0789e275c65..7e2545b4dff 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/layout.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/layout.tsx
@@ -58,6 +58,10 @@ export default async function TeamLayout(props: {
path: `/team/${params.team_slug}/${params.project_slug}/contracts`,
name: "Contracts",
},
+ {
+ path: `/team/${params.team_slug}/${params.project_slug}/nebula`,
+ name: "Nebula",
+ },
{
path: `/team/${params.team_slug}/${params.project_slug}/settings`,
name: "Settings",
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist-page-ui.client.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist-page-ui.client.tsx
new file mode 100644
index 00000000000..eb2fe260d70
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist-page-ui.client.tsx
@@ -0,0 +1,197 @@
+"use client";
+
+import { Spinner } from "@/components/ui/Spinner/Spinner";
+import { Button } from "@/components/ui/button";
+import { ToolTipLabel } from "@/components/ui/tooltip";
+import { useDashboardRouter } from "@/lib/DashboardRouter";
+import { useMutation } from "@tanstack/react-query";
+import { ArrowRightIcon, CheckIcon, OrbitIcon, ShareIcon } from "lucide-react";
+import Link from "next/link";
+import { useState } from "react";
+import { toast } from "sonner";
+
+export function JoinNebulaWaitlistPageUI(props: {
+ onWaitlist: boolean;
+ joinWaitList: () => Promise;
+}) {
+ const router = useDashboardRouter();
+
+ return (
+
+ {/* Header */}
+
+
+
Nebula
+
+
+
+
+
+ {props.onWaitlist ? (
+ }
+ />
+ ) : (
+ {
+ router.refresh();
+ }}
+ />
+ }
+ />
+ )}
+
+
+ );
+}
+
+function ShareButton() {
+ const [isCopied, setIsCopied] = useState(false);
+
+ return (
+
+
+
+ );
+}
+
+function JoinWaitingListButton(props: {
+ joinWaitList: () => Promise;
+ onSuccess: () => void;
+}) {
+ const joinWaitListMutation = useMutation({
+ mutationFn: props.joinWaitList,
+ onSuccess: props.onSuccess,
+ });
+
+ return (
+
+ );
+}
+
+function CenteredCard(props: {
+ footer: React.ReactNode;
+ title: React.ReactNode;
+ description: string;
+}) {
+ return (
+
+
+ {/* fancy borders */}
+
+ {/* top */}
+
+ {/* bottom */}
+
+ {/* left */}
+
+ {/* right */}
+
+
+
+
+
+ {/* Icon */}
+
+
+
+
+
+ {props.title}
+
+
+
+
+
+ {props.description}
+
+
+
+
+ {props.footer}
+
+
+
+
+ );
+}
+
+function DashedBgDiv(props: {
+ className?: string;
+ type: "horizontal" | "vertical";
+}) {
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist-page.client.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist-page.client.tsx
new file mode 100644
index 00000000000..559b8bb3fa1
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist-page.client.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { joinTeamWaitlist } from "@/actions/joinWaitlist";
+import { JoinNebulaWaitlistPageUI } from "./nebula-waitlist-page-ui.client";
+
+export function JoinNebulaWaitlistPage(props: {
+ onWaitlist: boolean;
+ teamSlug: string;
+}) {
+ return (
+ {
+ await joinTeamWaitlist({
+ scope: "nebula",
+ teamSlug: props.teamSlug,
+ });
+ }}
+ />
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist.stories.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist.stories.tsx
new file mode 100644
index 00000000000..f01835d307b
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist.stories.tsx
@@ -0,0 +1,63 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { Toaster } from "sonner";
+import { mobileViewport } from "stories/utils";
+import { JoinNebulaWaitlistPageUI } from "./nebula-waitlist-page-ui.client";
+
+const meta = {
+ title: "nebula/waitlist",
+ component: Story,
+ parameters: {
+ nextjs: {
+ appDirectory: true,
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const NotInWaitingListDesktop: Story = {
+ args: {
+ inWaitlist: false,
+ },
+};
+
+export const InWaitingListDesktop: Story = {
+ args: {
+ inWaitlist: true,
+ },
+};
+
+export const NotInWaitingListMobile: Story = {
+ args: {
+ inWaitlist: false,
+ },
+ parameters: {
+ viewport: mobileViewport("iphone14"),
+ },
+};
+
+export const InWaitingListMobile: Story = {
+ args: {
+ inWaitlist: true,
+ },
+ parameters: {
+ viewport: mobileViewport("iphone14"),
+ },
+};
+
+function Story(props: {
+ inWaitlist: boolean;
+}) {
+ return (
+ <>
+ {
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+ }}
+ />
+
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/page.tsx
new file mode 100644
index 00000000000..1eceea3eec1
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/page.tsx
@@ -0,0 +1,39 @@
+import { getTeamBySlug, getTeamNebulaWaitList } from "@/api/team";
+import { redirect } from "next/navigation";
+import { JoinNebulaWaitlistPage } from "./components/nebula-waitlist-page.client";
+
+export default async function Page(props: {
+ params: Promise<{
+ team_slug: string;
+ project_slug: string;
+ }>;
+}) {
+ const params = await props.params;
+ const team = await getTeamBySlug(params.team_slug);
+
+ if (!team) {
+ redirect(
+ `/login?next=${encodeURIComponent(`/team/${params.team_slug}/${params.project_slug}/nebula`)}`,
+ );
+ }
+
+ const nebulaWaitList = await getTeamNebulaWaitList(team.slug);
+
+ // this should never happen
+ if (!nebulaWaitList) {
+ return (
+
+
+ Something went wrong trying to fetch the nebula waitlist
+
+
+ );
+ }
+
+ return (
+
+ );
+}