Skip to content

Commit c3da78a

Browse files
committed
Add Nebula Waitlist page
1 parent 040e478 commit c3da78a

File tree

9 files changed

+415
-5
lines changed

9 files changed

+415
-5
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"use server";
2+
3+
import { getAuthToken } from "../../app/api/lib/getAuthToken";
4+
import { API_SERVER_URL } from "../constants/env";
5+
6+
export async function joinTeamWaitlist(options: {
7+
teamSlug: string;
8+
// currently only 'nebula' is supported
9+
scope: "nebula";
10+
}) {
11+
const { teamSlug, scope } = options;
12+
const token = await getAuthToken();
13+
14+
if (!token) {
15+
throw new Error("No Auth token");
16+
}
17+
18+
const res = await fetch(`${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist`, {
19+
method: "POST",
20+
headers: {
21+
"Content-Type": "application/json",
22+
Authorization: `Bearer ${token}`,
23+
},
24+
body: JSON.stringify({
25+
scope,
26+
}),
27+
});
28+
29+
if (!res.ok) {
30+
throw new Error("Failed to join waitlist");
31+
}
32+
33+
return true;
34+
}

apps/dashboard/src/@/api/team.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import "server-only";
22
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
33
import { API_SERVER_URL } from "@/constants/env";
44
import { cookies } from "next/headers";
5+
import { getAuthToken } from "../../app/api/lib/getAuthToken";
56

67
export type Team = {
78
id: string;
@@ -18,11 +19,7 @@ export type Team = {
1819
};
1920

2021
export async function getTeamBySlug(slug: string) {
21-
const cookiesManager = await cookies();
22-
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
23-
const token = activeAccount
24-
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
25-
: null;
22+
const token = await getAuthToken();
2623

2724
if (!token) {
2825
return null;
@@ -60,3 +57,31 @@ export async function getTeams() {
6057
}
6158
return [];
6259
}
60+
61+
type TeamNebulWaitList = {
62+
onWaitlist: boolean;
63+
createdAt: null | string;
64+
};
65+
66+
export async function getTeamNebuleWaitList(teamSlug: string) {
67+
const token = await getAuthToken();
68+
69+
if (!token) {
70+
return null;
71+
}
72+
73+
const res = await fetch(
74+
`${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist?scope=nebula`,
75+
{
76+
headers: {
77+
Authorization: `Bearer ${token}`,
78+
},
79+
},
80+
);
81+
82+
if (res.ok) {
83+
return (await res.json()).result as TeamNebulWaitList;
84+
}
85+
86+
return null;
87+
}

apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export default async function TeamLayout(props: {
5555
path: `/team/${params.team_slug}/~/ecosystem`,
5656
name: "Ecosystems",
5757
},
58+
{
59+
path: `/team/${params.team_slug}/~/nebula`,
60+
name: "Nebula",
61+
},
5862
{
5963
path: `/team/${params.team_slug}/~/usage`,
6064
name: "Usage",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { getTeamBySlug, getTeamNebuleWaitList } from "@/api/team";
2+
import { redirect } from "next/navigation";
3+
import { JoinNebulaWaitlistPage } from "../../../[project_slug]/nebula/components/nebula-waitlist-page.client";
4+
5+
export default async function Page(props: {
6+
params: Promise<{
7+
team_slug: string;
8+
}>;
9+
}) {
10+
const params = await props.params;
11+
const team = await getTeamBySlug(params.team_slug);
12+
13+
if (!team) {
14+
redirect(
15+
`/login?next=${encodeURIComponent(`/team/${params.team_slug}/~/nebula`)}`,
16+
);
17+
}
18+
19+
const nebulaWaitList = await getTeamNebuleWaitList(team.slug);
20+
21+
// this should never happen
22+
if (!nebulaWaitList) {
23+
return (
24+
<div className="container flex grow flex-col py-8">
25+
<div className="flex min-h-[300px] grow flex-col items-center justify-center rounded-lg border p-6 text-destructive-text">
26+
Something went wrong trying to fetch the nebula waitlist
27+
</div>
28+
</div>
29+
);
30+
}
31+
32+
return (
33+
<JoinNebulaWaitlistPage
34+
onWaitlist={nebulaWaitList.onWaitlist}
35+
teamSlug={team.slug}
36+
/>
37+
);
38+
}

apps/dashboard/src/app/team/[team_slug]/[project_slug]/layout.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export default async function TeamLayout(props: {
5858
path: `/team/${params.team_slug}/${params.project_slug}/contracts`,
5959
name: "Contracts",
6060
},
61+
{
62+
path: `/team/${params.team_slug}/${params.project_slug}/nebula`,
63+
name: "Nebula",
64+
},
6165
{
6266
path: `/team/${params.team_slug}/${params.project_slug}/settings`,
6367
name: "Settings",
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"use client";
2+
3+
import { Spinner } from "@/components/ui/Spinner/Spinner";
4+
import { Button } from "@/components/ui/button";
5+
import { ToolTipLabel } from "@/components/ui/tooltip";
6+
import { useDashboardRouter } from "@/lib/DashboardRouter";
7+
import { useMutation } from "@tanstack/react-query";
8+
import { ArrowRightIcon, CheckIcon, OrbitIcon, ShareIcon } from "lucide-react";
9+
import { useState } from "react";
10+
import { toast } from "sonner";
11+
12+
export function JoinNebulaWaitlistPageUI(props: {
13+
onWaitlist: boolean;
14+
joinWaitList: () => Promise<void>;
15+
}) {
16+
const router = useDashboardRouter();
17+
18+
return (
19+
<div className="container flex grow flex-col overflow-hidden py-24">
20+
{props.onWaitlist ? (
21+
<CenteredCard
22+
key="on-waitlist"
23+
title="You're on the waitlist"
24+
description="You should receive access to Nebula soon!"
25+
footer={<ShareButton />}
26+
/>
27+
) : (
28+
<CenteredCard
29+
key="not-on-waitlist"
30+
title="Nebula"
31+
description="Blockchain-first AI that can read & write onchain in realtime."
32+
footer={
33+
<JoinWaitingListButton
34+
joinWaitList={props.joinWaitList}
35+
onSuccess={() => {
36+
router.refresh();
37+
}}
38+
/>
39+
}
40+
/>
41+
)}
42+
</div>
43+
);
44+
}
45+
46+
function ShareButton() {
47+
const [isCopied, setIsCopied] = useState(false);
48+
49+
return (
50+
<ToolTipLabel label="Copy Page Link">
51+
<Button
52+
variant="outline"
53+
className="gap-2"
54+
onClick={() => {
55+
navigator.clipboard.writeText("https://thirdweb.com/team/~/nebula");
56+
setIsCopied(true);
57+
setTimeout(() => setIsCopied(false), 1000);
58+
}}
59+
>
60+
Share
61+
{isCopied ? (
62+
<CheckIcon className="size-4 text-green-500" />
63+
) : (
64+
<ShareIcon className="size-4" />
65+
)}
66+
</Button>
67+
</ToolTipLabel>
68+
);
69+
}
70+
71+
function JoinWaitingListButton(props: {
72+
joinWaitList: () => Promise<void>;
73+
onSuccess: () => void;
74+
}) {
75+
const joinWaitListMutation = useMutation({
76+
mutationFn: props.joinWaitList,
77+
onSuccess: props.onSuccess,
78+
});
79+
80+
return (
81+
<Button
82+
className="gap-2 rounded-full"
83+
variant="primary"
84+
onClick={() => {
85+
const promise = joinWaitListMutation.mutateAsync();
86+
toast.promise(promise, {
87+
success: "Joined the waitlist!",
88+
error: "Failed to join waitlist",
89+
});
90+
}}
91+
>
92+
Join the waitlist
93+
{joinWaitListMutation.isPending ? (
94+
<Spinner className="size-4" />
95+
) : (
96+
<ArrowRightIcon className="size-4" />
97+
)}
98+
</Button>
99+
);
100+
}
101+
102+
function CenteredCard(props: {
103+
footer: React.ReactNode;
104+
title: React.ReactNode;
105+
description: string;
106+
}) {
107+
return (
108+
<div className="flex grow flex-col items-center justify-center max-sm:px-4">
109+
<div className="relative flex min-h-[480px] w-full flex-col rounded-xl border bg-muted/50 p-2 lg:w-[480px]">
110+
{/* fancy borders */}
111+
<div className="">
112+
{/* top */}
113+
<DashedBgDiv
114+
className="-translate-x-1/2 -translate-y-5 absolute top-0 right-0 left-1/2 h-[1px] w-[calc(100%+200px)]"
115+
type="horizontal"
116+
/>
117+
{/* bottom */}
118+
<DashedBgDiv
119+
className="-translate-x-1/2 absolute right-0 bottom-0 left-1/2 h-[1px] w-[calc(100%+200px)] translate-y-5"
120+
type="horizontal"
121+
/>
122+
{/* left */}
123+
<DashedBgDiv
124+
className="-translate-x-5 -translate-y-1/2 absolute top-1/2 left-0 h-[calc(100%+200px)] w-[1px]"
125+
type="vertical"
126+
/>
127+
{/* right */}
128+
<DashedBgDiv
129+
className="-translate-y-1/2 absolute top-1/2 right-0 h-[calc(100%+200px)] w-[1px] translate-x-5"
130+
type="vertical"
131+
/>
132+
</div>
133+
134+
<div className="flex grow items-center justify-center rounded-lg border p-4">
135+
<div className="flex flex-col items-center">
136+
{/* Icon */}
137+
<div className="rounded-xl border p-1">
138+
<div className="rounded-lg border bg-muted/50 p-2">
139+
<OrbitIcon className="size-5 text-muted-foreground" />
140+
</div>
141+
</div>
142+
143+
<div className="h-4" />
144+
145+
<h1 className="text-balance text-center font-semibold text-2xl tracking-tight md:text-3xl">
146+
{props.title}
147+
</h1>
148+
149+
<div className="h-2" />
150+
151+
<p className="text-center text-muted-foreground lg:px-8">
152+
{props.description}
153+
</p>
154+
155+
<div className="h-6" />
156+
157+
{props.footer}
158+
</div>
159+
</div>
160+
</div>
161+
</div>
162+
);
163+
}
164+
165+
function DashedBgDiv(props: {
166+
className?: string;
167+
type: "horizontal" | "vertical";
168+
}) {
169+
return (
170+
<div
171+
className={props.className}
172+
style={{
173+
backgroundImage: `linear-gradient(${props.type === "horizontal" ? "90deg" : "180deg"}, hsl(var(--foreground)/20%) 0 30%, transparent 0 100%)`,
174+
backgroundRepeat: "repeat",
175+
backgroundSize: "10px 10px",
176+
maskImage: `linear-gradient(${
177+
props.type === "horizontal" ? "to right" : "to bottom"
178+
}, rgba(0,0,0,0.1), black 20%, black 80%, rgba(0,0,0,0.1))`,
179+
}}
180+
/>
181+
);
182+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"use client";
2+
3+
import { joinTeamWaitlist } from "@/actions/joinWaitlist";
4+
import { JoinNebulaWaitlistPageUI } from "./nebula-waitlist-page-ui.client";
5+
6+
export function JoinNebulaWaitlistPage(props: {
7+
onWaitlist: boolean;
8+
teamSlug: string;
9+
}) {
10+
return (
11+
<JoinNebulaWaitlistPageUI
12+
onWaitlist={props.onWaitlist}
13+
joinWaitList={async () => {
14+
await joinTeamWaitlist({
15+
scope: "nebula",
16+
teamSlug: props.teamSlug,
17+
});
18+
}}
19+
/>
20+
);
21+
}

0 commit comments

Comments
 (0)