Skip to content

Commit b760f59

Browse files
committed
feat: Add image upload and removal functionality in MyProfile; enhance user experience with image preview and validation
1 parent 2617ae3 commit b760f59

1 file changed

Lines changed: 120 additions & 8 deletions

File tree

src/pages/user/MyProfile.tsx

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useState, useEffect } from 'react';
2-
import { Card, Button, Input, Label, Text, Avatar } from '@fluentui/react-components';
1+
import { useState, useEffect, useRef } from 'react';
2+
import { Card, Button, Input, Label, Text, Avatar, tokens } from '@fluentui/react-components';
3+
import { Camera24Regular, Delete24Regular } from '@fluentui/react-icons';
34
import { useForm, Controller } from 'react-hook-form';
45
import { useUser } from '../../hooks/useUser';
56
import { usersApi } from '../../components/apis/users';
@@ -31,8 +32,11 @@ export default function MyProfile() {
3132
const userCtx = useUser();
3233
const [isEditing, setIsEditing] = useState(false);
3334
const [loading, setLoading] = useState(false);
35+
const [imagePreview, setImagePreview] = useState<string | null>(null);
36+
const [imageError, setImageError] = useState<string | null>(null);
37+
const fileInputRef = useRef<HTMLInputElement>(null);
3438

35-
const { control, handleSubmit, reset, formState: { errors }, watch, setError, clearErrors } = useForm<ProfileFormInputs>({
39+
const { control, handleSubmit, reset, formState: { errors }, watch, setError, clearErrors, setValue } = useForm<ProfileFormInputs>({
3640
defaultValues: {
3741
userName: userCtx?.user?.userName || '',
3842
firstName: userCtx?.user?.firstName || '',
@@ -69,9 +73,61 @@ export default function MyProfile() {
6973
newPassword: '',
7074
confirmPassword: '',
7175
});
76+
setImagePreview(userCtx.user.userIMG || null);
7277
}
7378
}, [userCtx?.user, reset]);
7479

80+
// Handle image file selection
81+
const handleImageSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
82+
const file = event.target.files?.[0];
83+
setImageError(null);
84+
85+
if (!file) return;
86+
87+
// Validate file type
88+
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
89+
if (!allowedTypes.includes(file.type)) {
90+
setImageError('Please select a valid image file (JPEG, PNG, GIF, or WebP)');
91+
return;
92+
}
93+
94+
// Validate file size (5MB max)
95+
const maxSizeBytes = 5 * 1024 * 1024;
96+
if (file.size > maxSizeBytes) {
97+
setImageError('Image size must be less than 5MB');
98+
return;
99+
}
100+
101+
// Read and convert to base64
102+
const reader = new FileReader();
103+
reader.onload = (e) => {
104+
const base64 = e.target?.result as string;
105+
setImagePreview(base64);
106+
setValue('userIMG', base64);
107+
};
108+
reader.onerror = () => {
109+
setImageError('Failed to read image file');
110+
};
111+
reader.readAsDataURL(file);
112+
};
113+
114+
// Handle image removal
115+
const handleImageRemove = () => {
116+
setImagePreview(null);
117+
setValue('userIMG', null);
118+
setImageError(null);
119+
if (fileInputRef.current) {
120+
fileInputRef.current.value = '';
121+
}
122+
};
123+
124+
// Trigger file input click
125+
const handleImageClick = () => {
126+
if (isEditing && fileInputRef.current) {
127+
fileInputRef.current.click();
128+
}
129+
};
130+
75131
const onSubmit = async (data: ProfileFormInputs) => {
76132
setLoading(true);
77133
try {
@@ -176,6 +232,8 @@ export default function MyProfile() {
176232

177233
// Update context and local storage
178234
userCtx?.setUser(updatedUser);
235+
// Update image preview
236+
setImagePreview(updatedUser.userIMG ?? null);
179237
// Refresh form values to updated user's values
180238
reset({
181239
userName: updatedUser.userName || '',
@@ -211,14 +269,68 @@ export default function MyProfile() {
211269
{/* User Info Row */}
212270
<div className={mergeClasses(s.flexRowFit, s.spaceBetween)}>
213271
<div className={mergeClasses(s.flexRowFit, s.alignCenter, s.gap)}>
214-
<Avatar
215-
name={fullName}
216-
size={64}
217-
image={userCtx?.user?.userIMG ? { src: userCtx.user.userIMG } : undefined}
218-
/>
272+
{/* Profile Picture with Upload */}
273+
<div style={{ position: 'relative', display: 'inline-block' }}>
274+
<Avatar
275+
name={fullName}
276+
size={72}
277+
image={imagePreview ? { src: imagePreview } : undefined}
278+
style={{ cursor: isEditing ? 'pointer' : 'default' }}
279+
onClick={handleImageClick}
280+
/>
281+
{isEditing && (
282+
<div style={{
283+
position: 'absolute',
284+
bottom: 0,
285+
right: 0,
286+
display: 'flex',
287+
gap: '2px',
288+
}}>
289+
<Button
290+
appearance="primary"
291+
size="small"
292+
icon={<Camera24Regular />}
293+
onClick={handleImageClick}
294+
style={{
295+
minWidth: 'auto',
296+
padding: '4px',
297+
borderRadius: '50%',
298+
}}
299+
title="Upload photo"
300+
/>
301+
{imagePreview && (
302+
<Button
303+
appearance="secondary"
304+
size="small"
305+
icon={<Delete24Regular />}
306+
onClick={handleImageRemove}
307+
style={{
308+
minWidth: 'auto',
309+
padding: '4px',
310+
borderRadius: '50%',
311+
}}
312+
title="Remove photo"
313+
/>
314+
)}
315+
</div>
316+
)}
317+
{/* Hidden file input */}
318+
<input
319+
ref={fileInputRef}
320+
type="file"
321+
accept="image/jpeg,image/png,image/gif,image/webp"
322+
onChange={handleImageSelect}
323+
style={{ display: 'none' }}
324+
/>
325+
</div>
219326
<div className={mergeClasses(s.flexColFill, s.alignCenter)}>
220327
<Text weight='bold' >{fullName}</Text>
221328
<Text >@{username}</Text>
329+
{imageError && (
330+
<Text style={{ color: tokens.colorPaletteRedForeground1, fontSize: tokens.fontSizeBase200 }}>
331+
{imageError}
332+
</Text>
333+
)}
222334
</div>
223335
</div>
224336
<div className={mergeClasses(s.flexColFit, s.alignCenter)}>

0 commit comments

Comments
 (0)