Files
worklenz/worklenz-frontend/src/pages/settings/profile/profile-settings.tsx
chamikaJ 8fb33e311d feat(localization): enhance task drawer translations and UI consistency
- Updated translations for task drawer components in Albanian, German, Spanish, Portuguese, Chinese, and English to improve clarity and consistency.
- Added new keys for task progress, activity log, and time log forms to support enhanced user interactions.
- Improved placeholder texts, error messages, and button labels for better user experience across multiple languages.
- Refactored activity log component to utilize translation keys for dynamic content rendering.
2025-07-08 15:51:47 +05:30

252 lines
7.6 KiB
TypeScript

import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
import {
Button,
Card,
Flex,
Form,
Input,
Tooltip,
Typography,
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_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;