From fae444da662ac364b74a6c399f7ed3fc25d4f1f7 Mon Sep 17 00:00:00 2001 From: MananTank Date: Wed, 13 Nov 2024 19:11:04 +0000 Subject: [PATCH] [Dashboard] Feature: Add Nebula Waitlist page (#5406) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem solved Short description of the bug fixed or feature added --- ## PR-Codex overview This PR introduces a new feature for managing a "Nebula" waitlist within a team dashboard. It adds new routes, components, and API functions to facilitate joining the waitlist and fetching its status. ### Detailed summary - Added a new route for `Nebula` under the team and project slugs. - Implemented `JoinNebulaWaitlistPage` component for handling waitlist interactions. - Created `joinTeamWaitlist` API function for server-side waitlist management. - Added `getTeamNebulaWaitList` API function to fetch waitlist status. - Updated existing pages to integrate the new waitlist functionality. - Enhanced UI components for displaying waitlist status and actions. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- apps/dashboard/src/@/actions/joinWaitlist.ts | 34 +++ apps/dashboard/src/@/api/team.ts | 35 +++- .../app/team/[team_slug]/(team)/layout.tsx | 4 + .../team/[team_slug]/(team)/~/nebula/page.tsx | 38 ++++ .../[team_slug]/[project_slug]/layout.tsx | 4 + .../nebula-waitlist-page-ui.client.tsx | 197 ++++++++++++++++++ .../nebula-waitlist-page.client.tsx | 21 ++ .../components/nebula-waitlist.stories.tsx | 63 ++++++ .../[project_slug]/nebula/page.tsx | 39 ++++ 9 files changed, 430 insertions(+), 5 deletions(-) create mode 100644 apps/dashboard/src/@/actions/joinWaitlist.ts create mode 100644 apps/dashboard/src/app/team/[team_slug]/(team)/~/nebula/page.tsx create mode 100644 apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist-page-ui.client.tsx create mode 100644 apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist-page.client.tsx create mode 100644 apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/nebula-waitlist.stories.tsx create mode 100644 apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/page.tsx 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 ( + + ); +}