Skip to content

Commit 2ec8276

Browse files
committed
feat: enhance UserMenu to support username editing with validation and error handling
1 parent e335b26 commit 2ec8276

File tree

1 file changed

+87
-28
lines changed

1 file changed

+87
-28
lines changed

web/src/modules/shared/components/layout/UserMenu.tsx

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,76 @@ import {
88
faSignOutAlt,
99
} from '@fortawesome/free-solid-svg-icons';
1010
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
11-
import Image from 'next/image';
12-
import { useState } from 'react';
1311
import { LoggedUserData } from '@web/src/modules/auth/types/User';
12+
import Image from 'next/image';
13+
import { useEffect, useState } from 'react';
1414
import { UserMenuButton } from '../client/UserMenuButton';
15-
import { EditUsernameModal } from './EditUsernameModal';
1615
import { UserMenuLink, UserMenuSplitLine } from './UserMenuLink';
1716
import {
1817
Popover,
1918
PopoverArrow,
2019
PopoverContent,
2120
PopoverTrigger,
2221
} from './popover';
22+
import { SubmitHandler, useForm } from 'react-hook-form';
23+
import ClientAxios from '@web/src/lib/axios/ClientAxios';
24+
import { AxiosError } from 'axios';
25+
import toast from 'react-hot-toast';
2326

24-
export function UserMenu({ userData }: { userData: LoggedUserData }) {
27+
interface FormValues {
28+
username: string;
29+
}
30+
31+
export const UserMenu = ({ userData }: { userData: LoggedUserData }) => {
2532
const [isEditingUsername, setIsEditingUsername] = useState(false);
26-
const [error, setError] = useState<string>('');
33+
const [name, setName] = useState(userData.username);
34+
35+
const {
36+
handleSubmit,
37+
formState: { isSubmitting, errors },
38+
register,
39+
} = useForm<FormValues>();
40+
41+
const onSubmit: SubmitHandler<FormValues> = async (data) => {
42+
try {
43+
await ClientAxios.patch('/user/username', {
44+
username: data.username,
45+
});
46+
47+
toast.success('Username updated successfully');
48+
setIsEditingUsername(false);
49+
setName(data.username);
50+
} catch (error: unknown) {
51+
if ((error as any).isAxiosError) {
52+
const axiosError = error as AxiosError;
53+
54+
// verify for throttling limit error
55+
if (axiosError.response?.status === 429) {
56+
toast.error('Too many requests. Please try again later.');
57+
}
58+
59+
// verify for validation error
60+
if (axiosError.response?.status === 400) {
61+
toast.error('Invalid username');
62+
}
2763

28-
// ERRORS:
29-
// 'This username is not available! :('
30-
// 'Your username may only contain these characters: A-Z a-z 0-9 - _ .'
64+
// verify for unauthorized error
65+
if (axiosError.response?.status === 401) {
66+
toast.error('Unauthorized');
67+
}
68+
69+
return;
70+
}
71+
72+
toast.error('An error occurred. Please try again later.');
73+
}
74+
};
75+
76+
useEffect(() => {
77+
if (errors.username?.message) {
78+
toast.error(errors.username.message);
79+
}
80+
}, [errors.username?.message]);
3181

3282
return (
3383
<Popover onOpenChange={() => setIsEditingUsername(false)}>
@@ -60,7 +110,7 @@ export function UserMenu({ userData }: { userData: LoggedUserData }) {
60110
{!isEditingUsername ? (
61111
<>
62112
<h4 className='truncate font-semibold w-48 py-px'>
63-
{userData.username}
113+
{name}
64114
</h4>
65115
<button onClick={() => setIsEditingUsername(true)}>
66116
<FontAwesomeIcon
@@ -72,31 +122,40 @@ export function UserMenu({ userData }: { userData: LoggedUserData }) {
72122
</>
73123
) : (
74124
<>
75-
<input
76-
className='w-[calc(12rem-52px)] font-semibold bg-transparent border border-zinc-400 rounded-md px-1'
77-
defaultValue={userData.username}
78-
></input>
79-
<button onClick={() => setIsEditingUsername(false)}>
80-
<FontAwesomeIcon
81-
icon={faClose}
82-
size='lg'
83-
className='text-zinc-400 hover:text-red-500'
125+
<form onSubmit={handleSubmit(onSubmit)}>
126+
<input
127+
className='w-[calc(12rem-52px)] font-semibold bg-transparent border border-zinc-400 rounded-md px-1'
128+
defaultValue={name}
129+
{...register('username', {
130+
required: 'Username is required',
131+
pattern: {
132+
value: /^[a-zA-Z0-9-_.]{1,32}$/,
133+
message:
134+
'Your username may only contain these characters: A-Z a-z 0-9 - _ .',
135+
},
136+
})}
84137
/>
85-
</button>
86-
<button onClick={() => setIsEditingUsername(false)}>
87-
<FontAwesomeIcon
88-
icon={faCheck}
89-
size='lg'
90-
className='text-zinc-400 hover:text-green-500'
91-
/>
92-
</button>
138+
<button onClick={() => setIsEditingUsername(true)}>
139+
<FontAwesomeIcon
140+
icon={faClose}
141+
size='lg'
142+
className='text-zinc-400 hover:text-red-500'
143+
/>
144+
</button>
145+
<button disabled={isSubmitting} type='submit'>
146+
<FontAwesomeIcon
147+
icon={faCheck}
148+
size='lg'
149+
className='text-zinc-400 hover:text-green-500'
150+
/>
151+
</button>
152+
</form>
93153
</>
94154
)}
95155
</div>
96156
<p className='text-zinc-300 text-xs truncate'>{userData.email}</p>
97157
</div>
98158
</div>
99-
{error && <p className='text-sm text-red-400 px-4 pb-2'>{error}</p>}
100159

101160
<UserMenuSplitLine />
102161

@@ -110,4 +169,4 @@ export function UserMenu({ userData }: { userData: LoggedUserData }) {
110169
</PopoverContent>
111170
</Popover>
112171
);
113-
}
172+
};

0 commit comments

Comments
 (0)