Skip to content

Commit 67d04af

Browse files
arcoravenjnsdls
authored andcommitted
feat: create/delete teams
1 parent 56d27d4 commit 67d04af

File tree

16 files changed

+244
-27
lines changed

16 files changed

+244
-27
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"use server";
2+
import "server-only";
3+
4+
import { randomBytes } from "node:crypto";
5+
import type { Team } from "@/api/team";
6+
import { format } from "date-fns";
7+
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
8+
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
9+
10+
export async function createTeam(options?: {
11+
name?: string;
12+
slug?: string;
13+
}) {
14+
const token = await getAuthToken();
15+
16+
if (!token) {
17+
return {
18+
status: "error",
19+
errorMessage: "You are not authorized to perform this action",
20+
} as const;
21+
}
22+
23+
const res = await fetch(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams`, {
24+
method: "POST",
25+
headers: {
26+
Authorization: `Bearer ${token}`,
27+
"Content-Type": "application/json",
28+
},
29+
body: JSON.stringify({
30+
name:
31+
options?.name ?? `Your Projects ${format(new Date(), "MMM d yyyy")}`,
32+
slug: options?.slug ?? randomBytes(20).toString("hex"),
33+
billingEmail: null,
34+
image: null,
35+
}),
36+
});
37+
38+
if (!res.ok) {
39+
const reason = await res.text();
40+
console.error("failed to create team", {
41+
status: res.status,
42+
reason,
43+
});
44+
switch (res.status) {
45+
case 400: {
46+
return {
47+
status: "error",
48+
errorMessage: "Invalid team name or slug.",
49+
} as const;
50+
}
51+
case 401: {
52+
return {
53+
status: "error",
54+
errorMessage: "You are not authorized to perform this action.",
55+
} as const;
56+
}
57+
default: {
58+
return {
59+
status: "error",
60+
errorMessage: "An unknown error occurred.",
61+
} as const;
62+
}
63+
}
64+
}
65+
66+
const json = (await res.json()) as {
67+
result: Team;
68+
};
69+
70+
return {
71+
status: "success",
72+
data: json.result,
73+
} as const;
74+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"use server";
2+
import "server-only";
3+
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
4+
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
5+
6+
export async function deleteTeam(options: {
7+
teamId: string;
8+
}) {
9+
const token = await getAuthToken();
10+
if (!token) {
11+
return {
12+
status: "error",
13+
errorMessage: "You are not authorized to perform this action.",
14+
} as const;
15+
}
16+
17+
const res = await fetch(
18+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}`,
19+
{
20+
method: "DELETE",
21+
headers: {
22+
Authorization: `Bearer ${token}`,
23+
},
24+
},
25+
);
26+
// handle errors
27+
if (!res.ok) {
28+
const reason = await res.text();
29+
console.error("failed to delete team", {
30+
status: res.status,
31+
reason,
32+
});
33+
switch (res.status) {
34+
case 400: {
35+
return {
36+
status: "error",
37+
errorMessage: "Invalid team ID.",
38+
} as const;
39+
}
40+
case 401: {
41+
return {
42+
status: "error",
43+
errorMessage: "You are not authorized to perform this action.",
44+
} as const;
45+
}
46+
47+
case 403: {
48+
return {
49+
status: "error",
50+
errorMessage: "You do not have permission to delete this team.",
51+
} as const;
52+
}
53+
case 404: {
54+
return {
55+
status: "error",
56+
errorMessage: "Team not found.",
57+
} as const;
58+
}
59+
default: {
60+
return {
61+
status: "error",
62+
errorMessage: "An unknown error occurred.",
63+
} as const;
64+
}
65+
}
66+
}
67+
return {
68+
status: "success",
69+
} as const;
70+
}

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: 17 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";
@@ -53,6 +55,21 @@ export function AccountHeader(props: {
5355
team,
5456
isOpen: true,
5557
}),
58+
createTeam: () => {
59+
toast.promise(
60+
createTeam().then((res) => {
61+
if (res.status === "error") {
62+
throw new Error(res.errorMessage);
63+
}
64+
router.push(`/team/${res.data.slug}`);
65+
}),
66+
{
67+
loading: "Creating team",
68+
success: "Team created",
69+
error: "Failed to create team",
70+
},
71+
);
72+
},
5673
account: props.account,
5774
client: props.client,
5875
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
@@ -18,6 +18,7 @@ export type AccountHeaderCompProps = {
1818
connectButton: React.ReactNode;
1919
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
2020
createProject: (team: Team) => void;
21+
createTeam: () => void;
2122
account: Pick<Account, "email" | "id" | "image">;
2223
client: ThirdwebClient;
2324
accountAddress: string;
@@ -59,6 +60,7 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
5960
teamsAndProjects={props.teamsAndProjects}
6061
focus="team-selection"
6162
createProject={props.createProject}
63+
createTeam={props.createTeam}
6264
account={props.account}
6365
client={props.client}
6466
/>
@@ -110,6 +112,7 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
110112
upgradeTeamLink={undefined}
111113
account={props.account}
112114
client={props.client}
115+
createTeam={props.createTeam}
113116
/>
114117
)}
115118
</div>

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

Lines changed: 24 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,22 @@ export function AccountTeamsUI(props: {
3538
.includes(teamSearchValue.toLowerCase());
3639
});
3740

41+
const createTeamAndRedirect = () => {
42+
toast.promise(
43+
createTeam().then((res) => {
44+
if (res.status === "error") {
45+
throw new Error(res.errorMessage);
46+
}
47+
router.push(`/team/${res.data.slug}`);
48+
}),
49+
{
50+
loading: "Creating team",
51+
success: "Team created",
52+
error: "Failed to create team",
53+
},
54+
);
55+
};
56+
3857
return (
3958
<div>
4059
<div className="flex flex-col items-start gap-4 lg:flex-row lg:justify-between">
@@ -45,12 +64,10 @@ export function AccountTeamsUI(props: {
4564
</p>
4665
</div>
4766

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>
67+
<Button className="gap-2 max-sm:w-full" onClick={createTeamAndRedirect}>
68+
<PlusIcon className="size-4" />
69+
Create Team
70+
</Button>
5471
</div>
5572

5673
<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: 14 additions & 12 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,43 @@ 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+
const result = await deleteTeam({ teamId: props.teamId });
309+
if (result.status === "error") {
310+
throw new Error(result.errorMessage);
311+
}
310312
},
311313
onSuccess: () => {
312314
router.push("/team");
313315
},
314316
});
315317

316318
function handleDelete() {
317-
const promise = deleteTeam.mutateAsync();
319+
const promise = deleteTeamAndRedirect.mutateAsync();
318320
toast.promise(promise, {
319-
success: "Team deleted successfully",
321+
success: "Team deleted",
320322
error: "Failed to delete team",
321323
});
322324
}
323325

324-
if (props.enabled) {
326+
if (props.canDelete) {
325327
return (
326328
<DangerSettingCard
327329
title={title}
328330
description={description}
329331
buttonLabel={title}
330332
buttonOnClick={handleDelete}
331-
isPending={deleteTeam.isPending}
333+
isPending={deleteTeamAndRedirect.isPending}
332334
confirmationDialog={{
333335
title: `Are you sure you want to delete team "${props.teamName}" ?`,
334336
description: description,

0 commit comments

Comments
 (0)