Skip to content

Commit d649c61

Browse files
committed
feat: create/delete teams
1 parent c95c0dc commit d649c61

15 files changed

+154
-26
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use server";
2+
3+
import { randomBytes } from "node:crypto";
4+
import type { Team } from "@/api/team";
5+
import { format } from "date-fns";
6+
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
7+
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
8+
9+
export async function createTeam(options: {
10+
name?: string;
11+
slug?: string;
12+
}): Promise<
13+
| {
14+
ok: true;
15+
data: Team;
16+
}
17+
| {
18+
ok: false;
19+
errorMessage: string;
20+
}
21+
> {
22+
const token = await getAuthToken();
23+
24+
if (!token) {
25+
return {
26+
ok: false,
27+
errorMessage: "You are not authorized to perform this action",
28+
};
29+
}
30+
31+
const res = await fetch(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams`, {
32+
method: "POST",
33+
headers: {
34+
Authorization: `Bearer ${token}`,
35+
"Content-Type": "application/json",
36+
},
37+
body: JSON.stringify({
38+
name: options.name ?? `Your Projects ${format(new Date(), "MMM d yyyy")}`,
39+
slug: options.slug ?? randomBytes(20).toString("hex"),
40+
billingEmail: null,
41+
image: null,
42+
}),
43+
});
44+
45+
if (!res.ok) {
46+
return {
47+
ok: false,
48+
errorMessage: await res.text(),
49+
};
50+
}
51+
52+
const json = await res.json();
53+
54+
return {
55+
ok: true,
56+
data: json.result,
57+
};
58+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use server";
2+
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
3+
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
4+
5+
export async function deleteTeam(options: {
6+
teamId: string;
7+
}): Promise<void> {
8+
const token = await getAuthToken();
9+
if (!token) {
10+
throw new Error("You are not authorized to perform this action");
11+
}
12+
13+
const res = await fetch(
14+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}`,
15+
{
16+
method: "DELETE",
17+
headers: {
18+
Authorization: `Bearer ${token}`,
19+
},
20+
},
21+
);
22+
if (!res.ok) {
23+
throw new Error(await res.text());
24+
}
25+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export async function getTeams() {
6767
return null;
6868
}
6969

70+
/** @deprecated */
7071
export async function getDefaultTeam() {
7172
const token = await getAuthToken();
7273
if (!token) {

apps/dashboard/src/app/(app)/account/components/AccountHeader.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"use client";
22

3+
import { createTeam } from "@/actions/createTeam";
34
import type { Project } from "@/api/projects";
45
import type { Team } from "@/api/team";
56
import { useDashboardRouter } from "@/lib/DashboardRouter";
67
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
78
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
89
import { LazyCreateProjectDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
910
import { useCallback, useState } from "react";
11+
import { toast } from "sonner";
1012
import type { ThirdwebClient } from "thirdweb";
1113
import { useActiveWallet, useDisconnect } from "thirdweb/react";
1214
import { doLogout } from "../../login/auth-actions";
@@ -57,6 +59,14 @@ export function AccountHeader(props: {
5759
team,
5860
isOpen: true,
5961
}),
62+
createTeam: async () => {
63+
const result = await createTeam({});
64+
if (!result.ok) {
65+
toast.error("Failed to create team");
66+
return;
67+
}
68+
router.push(`/team/${result.data.slug}`);
69+
},
6070
account: props.account,
6171
client: props.client,
6272
accountAddress: props.accountAddress,

apps/dashboard/src/app/(app)/account/components/AccountHeaderUI.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function Variants(props: {
5959
accountAddress={accountAddressStub}
6060
connectButton={<ConnectButtonStub />}
6161
createProject={() => {}}
62+
createTeam={() => {}}
6263
account={{
6364
id: "foo",
6465

apps/dashboard/src/app/(app)/account/components/AccountHeaderUI.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type AccountHeaderCompProps = {
2121
connectButton: React.ReactNode;
2222
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
2323
createProject: (team: Team) => void;
24+
createTeam: () => void;
2425
account: Pick<Account, "email" | "id" | "image">;
2526
client: ThirdwebClient;
2627
accountAddress: string;
@@ -64,6 +65,7 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
6465
teamsAndProjects={props.teamsAndProjects}
6566
focus="team-selection"
6667
createProject={props.createProject}
68+
createTeam={props.createTeam}
6769
account={props.account}
6870
client={props.client}
6971
/>
@@ -117,6 +119,7 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
117119
upgradeTeamLink={undefined}
118120
account={props.account}
119121
client={props.client}
122+
createTeam={props.createTeam}
120123
/>
121124
)}
122125
</div>

apps/dashboard/src/app/(app)/account/overview/AccountTeamsUI.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { createTeam } from "@/actions/createTeam";
34
import type { Team } from "@/api/team";
45
import type { TeamAccountRole } from "@/api/team-members";
56
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
@@ -10,10 +11,11 @@ import {
1011
DropdownMenuItem,
1112
DropdownMenuTrigger,
1213
} from "@/components/ui/dropdown-menu";
13-
import { ToolTipLabel } from "@/components/ui/tooltip";
14+
import { useDashboardRouter } from "@/lib/DashboardRouter";
1415
import { EllipsisIcon, PlusIcon } from "lucide-react";
1516
import Link from "next/link";
1617
import { useState } from "react";
18+
import { toast } from "sonner";
1719
import type { ThirdwebClient } from "thirdweb";
1820
import { TeamPlanBadge } from "../../components/TeamPlanBadge";
1921
import { getValidTeamPlan } from "../../team/components/TeamHeader/getValidTeamPlan";
@@ -26,6 +28,7 @@ export function AccountTeamsUI(props: {
2628
}[];
2729
client: ThirdwebClient;
2830
}) {
31+
const router = useDashboardRouter();
2932
const [teamSearchValue, setTeamSearchValue] = useState("");
3033
const teamsToShow = !teamSearchValue
3134
? props.teamsWithRole
@@ -35,6 +38,15 @@ export function AccountTeamsUI(props: {
3538
.includes(teamSearchValue.toLowerCase());
3639
});
3740

41+
const createTeamAndRedirect = async () => {
42+
const result = await createTeam({});
43+
if (!result.ok) {
44+
toast.error("Failed to create team");
45+
return;
46+
}
47+
router.push(`/team/${result.data.slug}`);
48+
};
49+
3850
return (
3951
<div>
4052
<div className="flex flex-col items-start gap-4 lg:flex-row lg:justify-between">
@@ -45,12 +57,10 @@ export function AccountTeamsUI(props: {
4557
</p>
4658
</div>
4759

48-
<ToolTipLabel label="Coming Soon">
49-
<Button disabled className="gap-2 max-sm:w-full">
50-
<PlusIcon className="size-4" />
51-
Create Team
52-
</Button>
53-
</ToolTipLabel>
60+
<Button className="gap-2 max-sm:w-full" onClick={createTeamAndRedirect}>
61+
<PlusIcon className="size-4" />
62+
Create Team
63+
</Button>
5464
</div>
5565

5666
<div className="h-4" />

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/general/GeneralSettingsPage.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ function ComponentVariants() {
6262
await new Promise((resolve) => setTimeout(resolve, 1000));
6363
}}
6464
/>
65-
<DeleteTeamCard enabled={true} teamName="foo" />
66-
<DeleteTeamCard enabled={false} teamName="foo" />
65+
<DeleteTeamCard canDelete={true} teamId="1" teamName="foo" />
66+
<DeleteTeamCard canDelete={false} teamId="2" teamName="foo" />
6767
</div>
6868
</div>
6969
</div>

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { deleteTeam } from "@/actions/deleteTeam";
34
import type { Team } from "@/api/team";
45
import type { VerifiedDomainResponse } from "@/api/verified-domain";
56
import { DangerSettingCard } from "@/components/blocks/DangerSettingCard";
@@ -35,7 +36,6 @@ export function TeamGeneralSettingsPageUI(props: {
3536
client: ThirdwebClient;
3637
leaveTeam: () => Promise<void>;
3738
}) {
38-
const hasPermissionToDelete = false; // TODO
3939
return (
4040
<div className="flex flex-col gap-8">
4141
<TeamNameFormControl
@@ -60,8 +60,9 @@ export function TeamGeneralSettingsPageUI(props: {
6060

6161
<LeaveTeamCard teamName={props.team.name} leaveTeam={props.leaveTeam} />
6262
<DeleteTeamCard
63-
enabled={hasPermissionToDelete}
63+
teamId={props.team.id}
6464
teamName={props.team.name}
65+
canDelete={props.isOwnerAccount}
6566
/>
6667
</div>
6768
);
@@ -293,42 +294,40 @@ export function LeaveTeamCard(props: {
293294
}
294295

295296
export function DeleteTeamCard(props: {
296-
enabled: boolean;
297+
canDelete: boolean;
298+
teamId: string;
297299
teamName: string;
298300
}) {
299301
const router = useDashboardRouter();
300302
const title = "Delete Team";
301303
const description =
302304
"Permanently remove your team and all of its contents from the thirdweb platform. This action is not reversible - please continue with caution.";
303305

304-
// TODO
305-
const deleteTeam = useMutation({
306+
const deleteTeamAndRedirect = useMutation({
306307
mutationFn: async () => {
307-
await new Promise((resolve) => setTimeout(resolve, 3000));
308-
console.log("Deleting team");
309-
throw new Error("Not implemented");
308+
await deleteTeam({ teamId: props.teamId });
310309
},
311310
onSuccess: () => {
312311
router.push("/team");
313312
},
314313
});
315314

316315
function handleDelete() {
317-
const promise = deleteTeam.mutateAsync();
316+
const promise = deleteTeamAndRedirect.mutateAsync();
318317
toast.promise(promise, {
319318
success: "Team deleted successfully",
320319
error: "Failed to delete team",
321320
});
322321
}
323322

324-
if (props.enabled) {
323+
if (props.canDelete) {
325324
return (
326325
<DangerSettingCard
327326
title={title}
328327
description={description}
329328
buttonLabel={title}
330329
buttonOnClick={handleDelete}
331-
isPending={deleteTeam.isPending}
330+
isPending={deleteTeamAndRedirect.isPending}
332331
confirmationDialog={{
333332
title: `Are you sure you want to delete team "${props.teamName}" ?`,
334333
description: description,

apps/dashboard/src/app/(app)/team/components/TeamHeader/TeamAndProjectSelectorPopoverButton.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type TeamSwitcherProps = {
2222
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
2323
focus: "project-selection" | "team-selection";
2424
createProject: (team: Team) => void;
25+
createTeam: () => void;
2526
account: Pick<Account, "email" | "id"> | undefined;
2627
client: ThirdwebClient;
2728
};
@@ -100,6 +101,10 @@ export function TeamAndProjectSelectorPopoverButton(props: TeamSwitcherProps) {
100101
}
101102
account={props.account}
102103
client={props.client}
104+
createTeam={() => {
105+
setOpen(false);
106+
props.createTeam();
107+
}}
103108
/>
104109

105110
{/* Right */}

apps/dashboard/src/app/(app)/team/components/TeamHeader/TeamHeaderUI.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ function Variant(props: {
155155
logout={() => {}}
156156
connectButton={<ConnectButtonStub />}
157157
createProject={() => {}}
158+
createTeam={() => {}}
158159
client={storybookThirdwebClient}
159160
getInboxNotifications={getInboxNotificationsStub}
160161
markNotificationAsRead={markNotificationAsReadStub}

apps/dashboard/src/app/(app)/team/components/TeamHeader/TeamHeaderUI.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type TeamHeaderCompProps = {
2929
logout: () => void;
3030
connectButton: React.ReactNode;
3131
createProject: (team: Team) => void;
32+
createTeam: () => void;
3233
client: ThirdwebClient;
3334
accountAddress: string;
3435
getInboxNotifications: () => Promise<NotificationMetadata[]>;
@@ -75,6 +76,7 @@ export function TeamHeaderDesktopUI(props: TeamHeaderCompProps) {
7576
teamsAndProjects={props.teamsAndProjects}
7677
focus="team-selection"
7778
createProject={props.createProject}
79+
createTeam={props.createTeam}
7880
account={props.account}
7981
client={props.client}
8082
/>
@@ -102,6 +104,7 @@ export function TeamHeaderDesktopUI(props: TeamHeaderCompProps) {
102104
teamsAndProjects={props.teamsAndProjects}
103105
focus="project-selection"
104106
createProject={props.createProject}
107+
createTeam={props.createTeam}
105108
account={props.account}
106109
client={props.client}
107110
/>
@@ -170,6 +173,9 @@ export function TeamHeaderMobileUI(props: TeamHeaderCompProps) {
170173
upgradeTeamLink={`/team/${currentTeam.slug}/settings`}
171174
account={props.account}
172175
client={props.client}
176+
createTeam={() => {
177+
alert("createTeam");
178+
}}
173179
/>
174180
</div>
175181

apps/dashboard/src/app/(app)/team/components/TeamHeader/TeamSelectionUI.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { Project } from "@/api/projects";
22
import type { Team } from "@/api/team";
33
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
44
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
5-
import { Badge } from "@/components/ui/badge";
65
import { Button } from "@/components/ui/button";
76
import { Separator } from "@/components/ui/separator";
87
import { cn } from "@/lib/utils";
@@ -25,6 +24,7 @@ export function TeamSelectionUI(props: {
2524
account: Pick<Account, "email" | "id" | "image"> | undefined;
2625
client: ThirdwebClient;
2726
isOnProjectPage: boolean;
27+
createTeam: () => void;
2828
}) {
2929
const { setHoveredTeam, currentTeam, teamsAndProjects } = props;
3030
const pathname = usePathname();
@@ -127,15 +127,12 @@ export function TeamSelectionUI(props: {
127127

128128
<li className="py-0.5">
129129
<Button
130-
className="w-full justify-start gap-2 px-2 disabled:pointer-events-auto disabled:cursor-not-allowed disabled:opacity-100"
130+
className="w-full justify-start gap-2 px-2"
131131
variant="ghost"
132-
disabled
132+
onClick={props.createTeam}
133133
>
134134
<CirclePlusIcon className="size-4 text-link-foreground" />
135135
Create Team
136-
<Badge className="ml-auto" variant="secondary">
137-
Soon™️
138-
</Badge>
139136
</Button>
140137
</li>
141138
</ul>

0 commit comments

Comments
 (0)