Skip to content

Commit f18b779

Browse files
committed
WIP - Plug in Project Settings Page UI with API
1 parent b148fa0 commit f18b779

File tree

4 files changed

+148
-22
lines changed

4 files changed

+148
-22
lines changed

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export function ProjectGeneralSettingsPage(props: {
5252
paths: EditProjectUIPaths;
5353
onKeyUpdated: (() => void) | undefined;
5454
wording: "project" | "api-key";
55+
// TODO: remove this when this component is not used in the project settings page
56+
hideFields?: {
57+
name: boolean;
58+
};
5559
}) {
5660
const updateMutation = useUpdateApiKey();
5761
const deleteMutation = useRevokeApiKey();
@@ -64,6 +68,7 @@ export function ProjectGeneralSettingsPage(props: {
6468
deleteMutation={deleteMutation}
6569
paths={props.paths}
6670
onKeyUpdated={props.onKeyUpdated}
71+
hideFields={props.hideFields}
6772
/>
6873
);
6974
}
@@ -84,6 +89,9 @@ interface EditApiKeyProps {
8489
deleteMutation: DeleteMutation;
8590
paths: EditProjectUIPaths;
8691
onKeyUpdated: (() => void) | undefined;
92+
hideFields?: {
93+
name: boolean;
94+
};
8795
}
8896

8997
type UpdateAPIForm = UseFormReturn<ApiKeyValidationSchema>;
@@ -214,12 +222,14 @@ export const ProjectGeneralSettingsPageUI: React.FC<EditApiKeyProps> = (
214222
autoComplete="off"
215223
>
216224
<div className="flex flex-col gap-8">
217-
<ProjectNameSetting
218-
form={form}
219-
updateMutation={updateMutation}
220-
handleSubmit={handleSubmit}
221-
wording={wording}
222-
/>
225+
{!props.hideFields?.name && (
226+
<ProjectNameSetting
227+
form={form}
228+
updateMutation={updateMutation}
229+
handleSubmit={handleSubmit}
230+
wording={wording}
231+
/>
232+
)}
223233

224234
<APIKeyDetails apiKey={apiKey} />
225235

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,106 @@
11
"use client";
22

3+
import type { Project } from "@/api/projects";
4+
import { SettingsCard } from "@/components/blocks/SettingsCard";
5+
import { Input } from "@/components/ui/input";
36
import { useDashboardRouter } from "@/lib/DashboardRouter";
47
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
8+
import { useMutation } from "@tanstack/react-query";
9+
import { useState } from "react";
10+
import { toast } from "sonner";
511
import { ProjectGeneralSettingsPage } from "./ProjectGeneralSettingsPage";
12+
import { updateProject } from "./updateProject";
613

714
export function ProjectGeneralSettingsPageForTeams(props: {
815
team_slug: string;
9-
project_slug: string;
16+
project: Project;
1017
apiKey: ApiKey;
18+
teamId: string;
1119
}) {
1220
const router = useDashboardRouter();
13-
const { team_slug, project_slug, apiKey } = props;
14-
const projectSettingsLayout = `/team/${team_slug}/${project_slug}/settings`;
21+
const { team_slug, project, apiKey } = props;
22+
const projectSettingsLayout = `/team/${team_slug}/${project.slug}/settings`;
1523

16-
// TODO - add a Project Image form field on this page
24+
return (
25+
<div className="flex flex-col gap-8">
26+
<ProjectNameSetting
27+
name={project.name}
28+
update={async (name) => {
29+
await updateProject({
30+
projectId: project.id,
31+
teamId: props.teamId, // TODO: remove this when project.teamId is fixed in api server
32+
value: {
33+
name,
34+
},
35+
});
36+
router.refresh();
37+
}}
38+
/>
39+
40+
{/* TODO - replace this when we have project services endpoints */}
41+
<ProjectGeneralSettingsPage
42+
apiKey={apiKey}
43+
paths={{
44+
aaConfig: `${projectSettingsLayout}/account-abstraction`,
45+
inAppConfig: `${projectSettingsLayout}/in-app-wallets`,
46+
payConfig: `${projectSettingsLayout}/pay`,
47+
afterDeleteRedirectTo: `/team/${team_slug}`,
48+
}}
49+
onKeyUpdated={() => {
50+
router.refresh();
51+
}}
52+
wording="project"
53+
hideFields={{
54+
name: true,
55+
}}
56+
/>
57+
</div>
58+
);
59+
}
60+
61+
function ProjectNameSetting(props: {
62+
name: string;
63+
update: (name: string) => Promise<void>;
64+
}) {
65+
const [name, setName] = useState(props.name);
66+
const updateName = useMutation({
67+
mutationFn: props.update,
68+
});
69+
70+
const errorText = name === "" ? "Project name is required" : undefined;
1771

1872
return (
19-
<ProjectGeneralSettingsPage
20-
apiKey={apiKey}
21-
paths={{
22-
aaConfig: `${projectSettingsLayout}/account-abstraction`,
23-
inAppConfig: `${projectSettingsLayout}/in-app-wallets`,
24-
payConfig: `${projectSettingsLayout}/pay`,
25-
afterDeleteRedirectTo: `/team/${team_slug}`,
73+
<SettingsCard
74+
header={{
75+
title: "Project Name",
76+
description:
77+
"Assign a name to identify your project on thirdweb dashboard",
2678
}}
27-
onKeyUpdated={() => {
28-
router.refresh();
79+
noPermissionText={undefined}
80+
errorText={errorText}
81+
saveButton={{
82+
onClick: () => {
83+
const promise = updateName.mutateAsync(name);
84+
toast.promise(promise, {
85+
success: "Project name updated successfully",
86+
error: "Failed to update project name",
87+
});
88+
},
89+
disabled: false,
90+
isPending: updateName.isPending,
2991
}}
30-
wording="project"
31-
/>
92+
bottomText="Please use 64 characters at maximum"
93+
>
94+
<Input
95+
autoFocus
96+
placeholder="My Project"
97+
type="text"
98+
value={name}
99+
onChange={(e) => {
100+
setName(e.target.value);
101+
}}
102+
className="max-w-[350px] bg-background"
103+
/>
104+
</SettingsCard>
32105
);
33106
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getProject } from "@/api/projects";
2+
import { getTeamBySlug } from "@/api/team";
23
import { notFound } from "next/navigation";
34
import { getAPIKeyForProjectId } from "../../../../api/lib/getAPIKeys";
45
import { ProjectGeneralSettingsPageForTeams } from "./ProjectGeneralSettingsPageForTeams";
@@ -7,6 +8,14 @@ export default async function Page(props: {
78
params: { team_slug: string; project_slug: string };
89
}) {
910
const { team_slug, project_slug } = props.params;
11+
12+
// TODO: remove this when project.teamId is fixed in api server
13+
const team = await getTeamBySlug(team_slug);
14+
15+
if (!team) {
16+
notFound();
17+
}
18+
1019
const project = await getProject(team_slug, project_slug);
1120

1221
if (!project) {
@@ -22,8 +31,9 @@ export default async function Page(props: {
2231
return (
2332
<ProjectGeneralSettingsPageForTeams
2433
apiKey={apiKey}
25-
project_slug={project_slug}
2634
team_slug={team_slug}
35+
project={project}
36+
teamId={team.id}
2737
/>
2838
);
2939
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use server";
2+
3+
import type { Project } from "@/api/projects";
4+
import { API_SERVER_URL } from "@/constants/env";
5+
import { getAuthToken } from "../../../../api/lib/getAuthToken";
6+
7+
export async function updateProject(params: {
8+
projectId: string;
9+
teamId: string;
10+
value: Partial<Project>;
11+
}) {
12+
const authToken = getAuthToken();
13+
14+
if (!authToken) {
15+
throw new Error("No auth token");
16+
}
17+
18+
const res = await fetch(
19+
`${API_SERVER_URL}/v1/teams/${params.teamId}/projects/${params.projectId}`,
20+
{
21+
method: "PUT",
22+
headers: {
23+
"Content-Type": "application/json",
24+
Authorization: `Bearer ${authToken}`,
25+
},
26+
body: JSON.stringify(params.value),
27+
},
28+
);
29+
30+
if (!res.ok) {
31+
throw new Error("failed to update team");
32+
}
33+
}

0 commit comments

Comments
 (0)