Skip to content

Commit b148fa0

Browse files
committed
Plug in Team settings page UI to API (#4888)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR primarily focuses on enhancing the `Team` functionality by adding billing features, updating team settings, and refining the UI components to support new properties such as `billingEmail` and `image`. It also improves the update mechanisms for team attributes. ### Detailed summary - Added `billingEmail` and `image` fields to the `Team` model. - Updated the `TeamGeneralSettingsPage` to accept `authToken` instead of `client`. - Implemented `updateTeam` function to handle team updates. - Modified UI components to support new fields and update methods. - Enhanced `FileInput` to handle fallback URLs. - Refactored `TeamNameFormControl` and `TeamSlugFormControl` to utilize `updateTeamField` for updates. - Integrated `resolveScheme` for avatar URL handling in `TeamAvatarFormControl`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 5c8766b commit b148fa0

File tree

9 files changed

+107
-45
lines changed

9 files changed

+107
-45
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ export type Team = {
99
slug: string;
1010
createdAt: string;
1111
updatedAt: string;
12-
deletedAt: string | null;
13-
bannedAt: string | null;
14-
// image: string; // -> TODO
12+
deletedAt?: string;
13+
bannedAt?: string;
14+
image?: string;
1515
billingPlan: "pro" | "growth" | "free";
1616
billingStatus: "validPayment" | (string & {}); // what's the other value?
17-
// billingEmail: string;
17+
billingEmail: string;
1818
// billingExternalId: string;
1919
// billingType: "STRIPE" | ??
2020
// billingCustomerPayload: ?? | null

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,9 @@ const testTeam: Team = {
3838
slug: "team-slug-foo-bar",
3939
createdAt: "2023-07-07T19:21:33.604Z",
4040
updatedAt: "2024-07-11T00:01:02.241Z",
41-
deletedAt: null,
42-
bannedAt: null,
4341
billingStatus: "validPayment",
4442
billingPlan: "free",
43+
billingEmail: "foo@example.com",
4544
};
4645

4746
function Story() {
@@ -52,6 +51,10 @@ function Story() {
5251
updateTeamImage={async () => {
5352
await new Promise((resolve) => setTimeout(resolve, 1000));
5453
}}
54+
updateTeamField={async (value) => {
55+
console.log(value);
56+
await new Promise((resolve) => setTimeout(resolve, 1000));
57+
}}
5558
/>
5659
<ComponentVariantions />
5760
<Toaster richColors />

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

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

33
import type { Team } from "@/api/team";
4-
import type { ThirdwebClient } from "thirdweb";
4+
import { getThirdwebClient } from "@/constants/thirdweb.server";
5+
import { useDashboardRouter } from "@/lib/DashboardRouter";
56
import { upload } from "thirdweb/storage";
67
import { TeamGeneralSettingsPageUI } from "./TeamGeneralSettingsPageUI";
8+
import { updateTeam } from "./updateTeam";
79

810
export function TeamGeneralSettingsPage(props: {
911
team: Team;
10-
client: ThirdwebClient;
12+
authToken: string;
1113
}) {
14+
const router = useDashboardRouter();
15+
1216
return (
1317
<TeamGeneralSettingsPageUI
1418
team={props.team}
19+
updateTeamField={async (teamValue) => {
20+
await updateTeam({
21+
teamId: props.team.id,
22+
value: teamValue,
23+
});
24+
25+
// Current page's slug is updated
26+
if (teamValue.slug) {
27+
router.replace(`/team/${teamValue.slug}/~/settings`);
28+
} else {
29+
router.refresh();
30+
}
31+
}}
1532
updateTeamImage={async (file) => {
33+
let uri: string | undefined = undefined;
34+
1635
if (file) {
1736
// upload to IPFS
18-
const uri = await upload({
19-
client: props.client,
37+
uri = await upload({
38+
client: getThirdwebClient(props.authToken),
2039
files: [file],
2140
});
22-
23-
// TODO - Implement updating the account image with uri
24-
console.log(uri);
25-
} else {
26-
// TODO - Implement deleting the account image
2741
}
2842

29-
throw new Error("Not implemented");
43+
await updateTeam({
44+
teamId: props.team.id,
45+
value: {
46+
image: uri,
47+
},
48+
});
3049
}}
3150
/>
3251
);

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

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,37 @@ import { DangerSettingCard } from "@/components/blocks/DangerSettingCard";
55
import { SettingsCard } from "@/components/blocks/SettingsCard";
66
import { CopyTextButton } from "@/components/ui/CopyTextButton";
77
import { Input } from "@/components/ui/input";
8+
import { useThirdwebClient } from "@/constants/thirdweb.client";
89
import { useDashboardRouter } from "@/lib/DashboardRouter";
910
import { useMutation } from "@tanstack/react-query";
1011
import { FileInput } from "components/shared/FileInput";
1112
import { useState } from "react";
1213
import { toast } from "sonner";
14+
import { resolveScheme } from "thirdweb/storage";
15+
16+
type UpdateTeamField = (team: Partial<Team>) => Promise<void>;
1317

1418
export function TeamGeneralSettingsPageUI(props: {
1519
team: Team;
1620
updateTeamImage: (file: File | undefined) => Promise<void>;
21+
updateTeamField: UpdateTeamField;
1722
}) {
1823
const hasPermissionToDelete = false; // TODO
1924
return (
2025
<div className="flex flex-col gap-8">
21-
<TeamNameFormControl team={props.team} />
22-
<TeamSlugFormControl team={props.team} />
23-
<TeamAvatarFormControl updateTeamImage={props.updateTeamImage} />
26+
<TeamNameFormControl
27+
team={props.team}
28+
updateTeamField={props.updateTeamField}
29+
/>
30+
<TeamSlugFormControl
31+
team={props.team}
32+
updateTeamField={props.updateTeamField}
33+
/>
34+
{/* THIS IS NOT WORKING - CAN"T UPDATE IMAGE */}
35+
<TeamAvatarFormControl
36+
updateTeamImage={props.updateTeamImage}
37+
avatar={props.team.image}
38+
/>
2439
<TeamIdCard team={props.team} />
2540
<LeaveTeamCard enabled={false} teamName={props.team.name} />
2641
<DeleteTeamCard
@@ -33,17 +48,13 @@ export function TeamGeneralSettingsPageUI(props: {
3348

3449
function TeamNameFormControl(props: {
3550
team: Team;
51+
updateTeamField: UpdateTeamField;
3652
}) {
3753
const [teamName, setTeamName] = useState(props.team.name);
3854
const maxTeamNameLength = 32;
3955

40-
// TODO - implement
4156
const updateTeamMutation = useMutation({
42-
mutationFn: async (teamName: string) => {
43-
await new Promise((resolve) => setTimeout(resolve, 3000));
44-
console.log("Updating team name to", teamName);
45-
throw new Error("Not implemented");
46-
},
57+
mutationFn: (name: string) => props.updateTeamField({ name }),
4758
});
4859

4960
function handleSave() {
@@ -82,20 +93,14 @@ function TeamNameFormControl(props: {
8293

8394
function TeamSlugFormControl(props: {
8495
team: Team;
96+
updateTeamField: (team: Partial<Team>) => Promise<void>;
8597
}) {
8698
const [teamSlug, setTeamSlug] = useState(props.team.slug);
8799
const [isTeamTaken] = useState(false);
88100
const maxTeamURLLength = 48;
89101

90-
// TODO - implement
91102
const updateTeamMutation = useMutation({
92-
mutationFn: async (_slug: string) => {
93-
// set isTeamTaken to true if team URL is taken
94-
// Fake loading
95-
await new Promise((resolve) => setTimeout(resolve, 3000));
96-
console.log("Updating team slug to", _slug);
97-
throw new Error("Not implemented");
98-
},
103+
mutationFn: (slug: string) => props.updateTeamField({ slug: slug }),
99104
});
100105

101106
function handleSave() {
@@ -144,8 +149,17 @@ function TeamSlugFormControl(props: {
144149

145150
function TeamAvatarFormControl(props: {
146151
updateTeamImage: (file: File | undefined) => Promise<void>;
152+
avatar: string | undefined;
147153
}) {
148-
const [teamAvatar, setTeamAvatar] = useState<File>(); // TODO: prefill with team avatar
154+
const client = useThirdwebClient();
155+
const teamUrl = props.avatar
156+
? resolveScheme({
157+
client: client,
158+
uri: props.avatar,
159+
})
160+
: undefined;
161+
162+
const [teamAvatar, setTeamAvatar] = useState<File | undefined>();
149163

150164
const updateTeamAvatarMutation = useMutation({
151165
mutationFn: async (_avatar: File | undefined) => {
@@ -186,6 +200,7 @@ function TeamAvatarFormControl(props: {
186200
setValue={setTeamAvatar}
187201
className="w-20 rounded-full lg:w-28"
188202
disableHelperText
203+
fileUrl={teamUrl}
189204
/>
190205
</div>
191206
</SettingsCard>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"use server";
2+
3+
import type { Team } from "@/api/team";
4+
import { API_SERVER_URL } from "@/constants/env";
5+
import { getAuthToken } from "../../../../../../api/lib/getAuthToken";
6+
7+
export async function updateTeam(params: {
8+
teamId: string;
9+
value: Partial<Team>;
10+
}) {
11+
const authToken = getAuthToken();
12+
13+
if (!authToken) {
14+
throw new Error("No auth token");
15+
}
16+
17+
const res = await fetch(`${API_SERVER_URL}/v1/teams/${params.teamId}`, {
18+
method: "PUT",
19+
headers: {
20+
"Content-Type": "application/json",
21+
Authorization: `Bearer ${authToken}`,
22+
},
23+
body: JSON.stringify(params.value),
24+
});
25+
26+
if (!res.ok) {
27+
throw new Error("failed to update team");
28+
}
29+
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,9 @@ const freeTeam: Team = {
4040
slug: "team-slug-foo-bar",
4141
createdAt: "2023-07-07T19:21:33.604Z",
4242
updatedAt: "2024-07-11T00:01:02.241Z",
43-
deletedAt: null,
44-
bannedAt: null,
4543
billingStatus: "validPayment",
4644
billingPlan: "free",
45+
billingEmail: "foo@example.com",
4746
};
4847

4948
const proTeam: Team = {
@@ -52,10 +51,9 @@ const proTeam: Team = {
5251
slug: "team-slug-foo-bar",
5352
createdAt: "2023-07-07T19:21:33.604Z",
5453
updatedAt: "2024-07-11T00:01:02.241Z",
55-
deletedAt: null,
56-
bannedAt: null,
5754
billingStatus: "validPayment",
5855
billingPlan: "pro",
56+
billingEmail: "foo@example.com",
5957
};
6058

6159
function createMemberStub(id: string, role: TeamAccountRole): TeamMember {

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { getTeamBySlug } from "@/api/team";
2-
import { getThirdwebClient } from "@/constants/thirdweb.server";
32
import { notFound } from "next/navigation";
43
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
54
import { TeamGeneralSettingsPage } from "./general/TeamGeneralSettingsPage";
@@ -15,7 +14,5 @@ export default async function Page(props: {
1514
notFound();
1615
}
1716

18-
return (
19-
<TeamGeneralSettingsPage team={team} client={getThirdwebClient(token)} />
20-
);
17+
return <TeamGeneralSettingsPage team={team} authToken={token} />;
2118
}

apps/dashboard/src/components/shared/FileInput.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface IFileInputProps {
2828
children?: React.ReactNode;
2929
className?: string;
3030
disableHelperText?: boolean;
31+
fileUrl?: string;
3132
}
3233

3334
export const FileInput: React.FC<IFileInputProps> = ({
@@ -45,6 +46,7 @@ export const FileInput: React.FC<IFileInputProps> = ({
4546
className,
4647
previewMaxWidth,
4748
disableHelperText,
49+
fileUrl: fileUrlFallback,
4850
}) => {
4951
const onDrop = useCallback<
5052
<T extends File>(
@@ -67,7 +69,7 @@ export const FileInput: React.FC<IFileInputProps> = ({
6769

6870
const file: File | null =
6971
typeof window !== "undefined" && value instanceof File ? value : null;
70-
const fileUrl = useImageFileOrUrl(value);
72+
const fileUrl = useImageFileOrUrl(value) || fileUrlFallback || "";
7173

7274
const helperTextOrFile = helperText
7375
? helperText

apps/dashboard/src/stories/stubs.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,9 @@ export function teamStub(
4545
billingStatus: "validPayment",
4646
name: `Team ${id}`,
4747
slug: `team-${id}`,
48-
bannedAt: null,
4948
createdAt: new Date().toISOString(),
50-
deletedAt: null,
5149
updatedAt: new Date().toISOString(),
50+
billingEmail: "foo@example.com",
5251
};
5352

5453
return team;

0 commit comments

Comments
 (0)