Skip to content

Commit 4b48c4c

Browse files
Merge pull request #298 from CivicDataLab/297-add-client-side-url-validation-for-social-media-fields
Add client-side URL validation for social media fields
2 parents 95dfa69 + 4d5b4f7 commit 4b48c4c

File tree

2 files changed

+85
-22
lines changed

2 files changed

+85
-22
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ next-env.d.ts
3939

4040
# generated graphql files
4141
/gql/generated/gql.ts
42-
/gql/generated/graphql.ts
42+
/gql/generated/graphql.ts
43+
/gql/generated/

app/[locale]/dashboard/[entityType]/[entitySlug]/profile/userProfile.tsx

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ const updateUserMutation: any = graphql(`
3535
}
3636
`);
3737

38+
const githubRegex = /^https:\/\/github\.com\/[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
39+
const linkedinRegex = /^https:\/\/(?:www\.)?linkedin\.com\/in\/[a-zA-Z0-9-]+\/?$/;
40+
const twitterRegex = /^https:\/\/(?:www\.)?(?:twitter\.com|x\.com)\/[a-zA-Z0-9_]+\/?$/;
41+
42+
const prettyField = (f: string) => {
43+
switch (f) {
44+
case 'github_profile':
45+
return 'GitHub URL';
46+
case 'linkedin_profile':
47+
return 'LinkedIn URL';
48+
case 'twitter_profile':
49+
return 'Twitter URL';
50+
default:
51+
return f.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
52+
}
53+
};
54+
3855
const UserProfile = () => {
3956
const params = useParams<{ entityType: string; entitySlug: string }>();
4057

@@ -70,9 +87,11 @@ const UserProfile = () => {
7087

7188
const { mutate, isLoading: editMutationLoading } = useMutation(
7289
(input: { input: UpdateUserInput }) =>
73-
GraphQL(updateUserMutation, {
74-
[params.entityType]: params.entitySlug,
75-
}, input),
90+
GraphQL(
91+
updateUserMutation,
92+
{ [params.entityType]: params.entitySlug },
93+
input
94+
),
7695
{
7796
onSuccess: (res: any) => {
7897
toast('User details updated successfully');
@@ -92,8 +111,36 @@ const UserProfile = () => {
92111
me: res.updateUser,
93112
});
94113
},
114+
95115
onError: (error: any) => {
96-
toast(`Error: ${error.message}`);
116+
if (typeof error?.message === 'string') {
117+
const message: string = error.message;
118+
119+
// Try to extract field errors
120+
const tryField = (field: string) => {
121+
const m = message.match(
122+
new RegExp(`'${field}'\\s*:\\s*\\['([^']+)'\\]`)
123+
);
124+
if (m?.[1]) {
125+
const prettyName = prettyField(field);
126+
const errorMsg = `${prettyName}: ${m[1]}`;
127+
toast.error(errorMsg);
128+
}
129+
return Boolean(m?.[1]);
130+
};
131+
132+
const anyMatched =
133+
tryField('github_profile') ||
134+
tryField('linkedin_profile') ||
135+
tryField('twitter_profile');
136+
137+
if (!anyMatched) {
138+
toast.error(`Error: ${message}`);
139+
}
140+
return;
141+
}
142+
143+
toast.error('An unexpected error occurred.');
97144
},
98145
}
99146
);
@@ -112,24 +159,36 @@ const UserProfile = () => {
112159
if (!formValidation) {
113160
toast('Please fill all the required fields');
114161
return;
115-
} else {
116-
const inputData: UpdateUserInput = {
117-
firstName: formData.firstName,
118-
lastName: formData.lastName,
119-
bio: formData.bio,
120-
email: formData.email,
121-
githubProfile: formData.githubProfile,
122-
linkedinProfile: formData.linkedinProfile,
123-
twitterProfile: formData.twitterProfile,
124-
location: formData.location,
125-
};
126-
127-
// Only add logo if it has changed
128-
if (formData.profilePicture instanceof File) {
129-
inputData.profilePicture = formData.profilePicture;
130-
}
131-
mutate({ input: inputData });
132162
}
163+
if (formData.githubProfile && !githubRegex.test(formData.githubProfile)) {
164+
toast.error('GitHub URL: Enter a valid URL.');
165+
return;
166+
}
167+
if (formData.linkedinProfile && !linkedinRegex.test(formData.linkedinProfile)) {
168+
toast.error('LinkedIn URL: Enter a valid URL.');
169+
return;
170+
}
171+
if (formData.twitterProfile && !twitterRegex.test(formData.twitterProfile)) {
172+
toast.error('Twitter URL: Enter a valid URL.');
173+
return;
174+
}
175+
176+
const inputData: UpdateUserInput = {
177+
firstName: formData.firstName,
178+
lastName: formData.lastName,
179+
bio: formData.bio,
180+
email: formData.email,
181+
githubProfile: formData.githubProfile,
182+
linkedinProfile: formData.linkedinProfile,
183+
twitterProfile: formData.twitterProfile,
184+
location: formData.location,
185+
};
186+
187+
// Only add logo if it has changed
188+
if (formData.profilePicture instanceof File) {
189+
inputData.profilePicture = formData.profilePicture;
190+
}
191+
mutate({ input: inputData });
133192
};
134193

135194
return (
@@ -182,20 +241,23 @@ const UserProfile = () => {
182241
label="Github Profile"
183242
name="githubProfile"
184243
type="url"
244+
placeholder="https://github.com/username"
185245
value={formData.githubProfile}
186246
onChange={(e) => setFormData({ ...formData, githubProfile: e })}
187247
/>
188248
<TextField
189249
label="Linkedin Profile"
190250
name="linkedinProfile"
191251
type="url"
252+
placeholder="https://linkedin.com/in/username"
192253
value={formData.linkedinProfile}
193254
onChange={(e) => setFormData({ ...formData, linkedinProfile: e })}
194255
/>
195256
<TextField
196257
label="Twitter Profile"
197258
name="twitterProfile"
198259
type="url"
260+
placeholder="https://twitter.com/username"
199261
value={formData.twitterProfile}
200262
onChange={(e) => setFormData({ ...formData, twitterProfile: e })}
201263
/>

0 commit comments

Comments
 (0)