|
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'; |
3 | 4 | import { useForm, Controller } from 'react-hook-form'; |
4 | 5 | import { useUser } from '../../hooks/useUser'; |
5 | 6 | import { usersApi } from '../../components/apis/users'; |
@@ -31,8 +32,11 @@ export default function MyProfile() { |
31 | 32 | const userCtx = useUser(); |
32 | 33 | const [isEditing, setIsEditing] = useState(false); |
33 | 34 | 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); |
34 | 38 |
|
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>({ |
36 | 40 | defaultValues: { |
37 | 41 | userName: userCtx?.user?.userName || '', |
38 | 42 | firstName: userCtx?.user?.firstName || '', |
@@ -69,9 +73,61 @@ export default function MyProfile() { |
69 | 73 | newPassword: '', |
70 | 74 | confirmPassword: '', |
71 | 75 | }); |
| 76 | + setImagePreview(userCtx.user.userIMG || null); |
72 | 77 | } |
73 | 78 | }, [userCtx?.user, reset]); |
74 | 79 |
|
| 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 | + |
75 | 131 | const onSubmit = async (data: ProfileFormInputs) => { |
76 | 132 | setLoading(true); |
77 | 133 | try { |
@@ -176,6 +232,8 @@ export default function MyProfile() { |
176 | 232 |
|
177 | 233 | // Update context and local storage |
178 | 234 | userCtx?.setUser(updatedUser); |
| 235 | + // Update image preview |
| 236 | + setImagePreview(updatedUser.userIMG ?? null); |
179 | 237 | // Refresh form values to updated user's values |
180 | 238 | reset({ |
181 | 239 | userName: updatedUser.userName || '', |
@@ -211,14 +269,68 @@ export default function MyProfile() { |
211 | 269 | {/* User Info Row */} |
212 | 270 | <div className={mergeClasses(s.flexRowFit, s.spaceBetween)}> |
213 | 271 | <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> |
219 | 326 | <div className={mergeClasses(s.flexColFill, s.alignCenter)}> |
220 | 327 | <Text weight='bold' >{fullName}</Text> |
221 | 328 | <Text >@{username}</Text> |
| 329 | + {imageError && ( |
| 330 | + <Text style={{ color: tokens.colorPaletteRedForeground1, fontSize: tokens.fontSizeBase200 }}> |
| 331 | + {imageError} |
| 332 | + </Text> |
| 333 | + )} |
222 | 334 | </div> |
223 | 335 | </div> |
224 | 336 | <div className={mergeClasses(s.flexColFit, s.alignCenter)}> |
|
0 commit comments