init
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
Form,
|
||||
GetProp,
|
||||
Input,
|
||||
Tooltip,
|
||||
Typography,
|
||||
UploadProps,
|
||||
Spin,
|
||||
Skeleton,
|
||||
} from 'antd';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { changeUserName, setUser } from '@features/user/userSlice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
|
||||
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||
import {
|
||||
evt_settings_profile_visit,
|
||||
evt_settings_profile_avatar_upload,
|
||||
evt_settings_profile_name_change,
|
||||
evt_settings_profile_picture_update,
|
||||
} from '@/shared/worklenz-analytics-events';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { getBase64 } from '@/utils/file-utils';
|
||||
|
||||
import './profile-settings.css';
|
||||
import { profileSettingsApiService } from '@/api/settings/profile/profile-settings.api.service';
|
||||
import taskAttachmentsApiService from '@/api/tasks/task-attachments.api.service';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { setSession } from '@/utils/session-helper';
|
||||
import { authApiService } from '@/api/auth/auth.api.service';
|
||||
|
||||
const ProfileSettings = () => {
|
||||
const { t } = useTranslation('settings/profile');
|
||||
const dispatch = useAppDispatch();
|
||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
const [imageUrl, setImageUrl] = useState<string>();
|
||||
const [form] = Form.useForm();
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useDocumentTitle(t('title') || 'Profile Settings');
|
||||
|
||||
useEffect(() => {
|
||||
trackMixpanelEvent(evt_settings_profile_visit);
|
||||
}, [trackMixpanelEvent]);
|
||||
|
||||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (uploading || !event.target.files || event.target.files.length === 0) return;
|
||||
|
||||
const file = event.target.files[0];
|
||||
setUploading(true);
|
||||
|
||||
try {
|
||||
const base64 = await getBase64(file);
|
||||
const res = await taskAttachmentsApiService.createAvatarAttachment({
|
||||
file: base64 as string,
|
||||
file_name: file.name,
|
||||
size: file.size,
|
||||
});
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_settings_profile_picture_update);
|
||||
const authorizeResponse = await authApiService.verify();
|
||||
if (authorizeResponse.authenticated) {
|
||||
setSession(authorizeResponse.user);
|
||||
dispatch(setUser(authorizeResponse.user));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error uploading avatar', e);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
|
||||
// Reset file input
|
||||
const dt = new DataTransfer();
|
||||
event.target.files = dt.files;
|
||||
};
|
||||
|
||||
const triggerFileInput = () => {
|
||||
if (!uploading) {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
};
|
||||
|
||||
const avatarPreview = (
|
||||
<div
|
||||
className="avatar-uploader ant-upload-select-picture-card"
|
||||
onClick={triggerFileInput}
|
||||
style={{
|
||||
width: '104px',
|
||||
height: '104px',
|
||||
cursor: uploading ? 'wait' : 'pointer',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{uploading && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1,
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24, color: 'white' }} spin />} />
|
||||
</div>
|
||||
)}
|
||||
{loading ? (
|
||||
<LoadingOutlined />
|
||||
) : imageUrl || currentSession?.avatar_url ? (
|
||||
<img
|
||||
src={imageUrl || currentSession?.avatar_url}
|
||||
alt="avatar"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: '4px' }}
|
||||
/>
|
||||
) : (
|
||||
<Flex align="center" justify="center" vertical gap={8} style={{ height: '100%' }}>
|
||||
<PlusOutlined />
|
||||
<Typography.Text>{t('upload')}</Typography.Text>
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleFormSubmit = async ({ name }: { name: string }) => {
|
||||
if (name === currentSession?.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUpdating(true);
|
||||
try {
|
||||
const res = await profileSettingsApiService.updateProfile({ name });
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_settings_profile_name_change, { newName: name });
|
||||
dispatch(changeUserName(name));
|
||||
// Refresh user session to get updated data
|
||||
const authorizeResponse = await authApiService.verify();
|
||||
if (authorizeResponse.authenticated) {
|
||||
setSession(authorizeResponse.user);
|
||||
dispatch(setUser(authorizeResponse.user));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error changing name', error);
|
||||
} finally {
|
||||
setUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card style={{ width: '100%' }}>
|
||||
{updating ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleFormSubmit}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
name: currentSession?.name,
|
||||
email: currentSession?.email,
|
||||
}}
|
||||
style={{ width: '100%', maxWidth: 350 }}
|
||||
>
|
||||
<Form.Item>
|
||||
<Tooltip title={t('avatarTooltip') || 'Click to upload an avatar'} placement="topLeft">
|
||||
{avatarPreview}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/png, image/jpg, image/jpeg"
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('nameLabel')}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('nameRequiredError'),
|
||||
},
|
||||
{
|
||||
min: 2,
|
||||
message: t('nameMinLengthError') || 'Name must be at least 2 characters',
|
||||
},
|
||||
{
|
||||
max: 50,
|
||||
message: t('nameMaxLengthError') || 'Name cannot exceed 50 characters',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input style={{ borderRadius: 4 }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label={t('emailLabel')}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('emailRequiredError'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input style={{ borderRadius: 4 }} disabled />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={updating}>
|
||||
{t('saveChanges')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
<Flex vertical gap={4} style={{ marginTop: 16 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('profileJoinedText', {
|
||||
date: currentSession?.created_at
|
||||
? new Date(currentSession.created_at).toLocaleDateString()
|
||||
: '',
|
||||
})}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('profileLastUpdatedText', {
|
||||
date: currentSession?.updated_at
|
||||
? new Date(currentSession.updated_at).toLocaleDateString()
|
||||
: '',
|
||||
})}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileSettings;
|
||||
Reference in New Issue
Block a user