In this tutorial, we will learn how to create a Dashboard layout with an Account section where the user can update their first name, last name, bio, and image using Next.js Server Actions. We will also cover file uploads in Next.js.
First, let's create our profile-actions.ts file inside the actions folder under data/actions directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"use server";
import { z } from "zod";
import qs from "qs";
import { getUserMeLoader } from "@/data/services/get-user-me-loader";
import { mutateData } from "@/data/services/mutate-data";
import { flattenAttributes } from "@/lib/utils";
export async function updateProfileAction(
userId: string,
prevState: any,
formData: FormData
) {
const rawFormData = Object.fromEntries(formData);
const query = qs.stringify({
populate: "*",
});
const payload = {
firstName: rawFormData.firstName,
lastName: rawFormData.lastName,
bio: rawFormData.bio,
};
const responseData = await mutateData(
"PUT",
`/api/users/${userId}?${query}`,
payload
);
if (!responseData) {
return {
...prevState,
strapiErrors: null,
message: "Ops! Something went wrong. Please try again.",
};
}
if (responseData.error) {
return {
...prevState,
strapiErrors: responseData.error,
message: "Failed to Register.",
};
}
const flattenedData = flattenAttributes(responseData);
return {
...prevState,
message: "Profile Updated",
data: flattenedData,
strapiErrors: null,
};
}
In this file, we are importing the necessary dependencies and creating a server action called updateProfileAction. This action takes three parameters: userId, prevState, and formData. The first parameter is the user's ID, which we will use to identify the user in our Strapi API. The second parameter is the previous state of the form, which we will use to update the form data after a successful update. The third parameter is the form data that the user submitted.
Next, let's create our profile-form.tsx file inside the forms folder under components/forms directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
"use client";
import React from "react";
import { useFormState } from "react-dom";
import { cn } from "@/lib/utils";
import { uploadProfileImageAction } from "@/data/actions/profile-actions";
import { SubmitButton } from "@/components/custom/SubmitButton";
import ImagePicker from "@/components/custom/ImagePicker";
import { ZodErrors } from "@/components/custom/ZodErrors";
import { StrapiErrors } from "@/components/custom/StrapiErrors";
interface ProfileImageFormProps {
id: string;
url: string;
alternativeText: string;
}
const initialState = {
message: null,
data: null,
strapiErrors: null,
zodErrors: null,
};
export function ProfileImageForm({
data,
className,
}: {
data: Readonly<ProfileImageFormProps>,
className?: string,
}) {
const uploadProfileImageWithIdAction = uploadProfileImageAction.bind(
null,
data?.id
);
const [formState, formAction] = useFormState(
uploadProfileImageWithIdAction,
initialState
);
return (
<form className={cn("space-y-4", className)} action={formAction}>
<div className="">
<ImagePicker
id="image"
name="image"
label="Profile Image"
defaultValue={data?.url || ""}
/>
<ZodErrors error={formState.zodErrors?.image} />
<StrapiErrors error={formState.strapiErrors} />
</div>
<div className="flex justify-end">
<SubmitButton text="Update Image" loadingText="Saving Image" />
</div>
</form>
);
}
In this file, we are importing the necessary dependencies and creating a client component called ProfileImageForm. This component takes two parameters: data and className. The first parameter is an object containing the user's profile image information, which includes the ID, URL, and alternative text of the image. The second parameter is an optional string that specifies additional CSS classes to apply to the form.
Next, let's create our get-user-me-loader.ts file inside the services folder under data/services directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import { getAuthToken } from "./get-token";
import { mutateData } from "./mutate-data";
import { flattenAttributes } from "@/lib/utils";
import { getStrapiURL } from "@/lib/utils";
export async function getUserMeLoader() {
const authToken = await getAuthToken();
if (!authToken) throw new Error("No auth token found");
const baseUrl = getStrapiURL();
const url = new URL("/api/users/me", baseUrl);
try {
const response = await fetch(url, {
headers: { Authorization: `Bearer ${authToken}` },
});
const dataResponse = await response.json();
return flattenAttributes(dataResponse);
} catch (error) {
console.error("Error getting user me:", error);
throw error;
}
}
In this file, we are importing the necessary dependencies and creating a server action called getUserMeLoader. This action takes no parameters and returns an object containing the user's profile information.
Next, let's create our mutate-data.ts file inside the services folder under data/services directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { getAuthToken } from "./get-token";
import { getStrapiURL } from "@/lib/utils";
export async function mutateData(method: string, path: string, payload?: any) {
const baseUrl = getStrapiURL();
const authToken = await getAuthToken();
ififififififififififififififififififififififififififififififif