mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? Fix: Profile page UI adjustment ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import { transformFile2Base64 } from '@/utils/file-util';
|
import { transformFile2Base64 } from '@/utils/file-util';
|
||||||
import { Pencil, Upload, XIcon } from 'lucide-react';
|
import { Pencil, Plus, XIcon } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
ChangeEventHandler,
|
ChangeEventHandler,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
@ -12,10 +12,14 @@ import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
|||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Input } from './ui/input';
|
import { Input } from './ui/input';
|
||||||
|
|
||||||
type AvatarUploadProps = { value?: string; onChange?: (value: string) => void };
|
type AvatarUploadProps = {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
tips?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
|
export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
|
||||||
function AvatarUpload({ value, onChange }, ref) {
|
function AvatarUpload({ value, onChange, tips }, ref) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64
|
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64
|
||||||
|
|
||||||
@ -47,9 +51,9 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
|
|||||||
<div className="flex justify-start items-end space-x-2">
|
<div className="flex justify-start items-end space-x-2">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
{!avatarBase64Str ? (
|
{!avatarBase64Str ? (
|
||||||
<div className="w-[64px] h-[64px] grid place-content-center border border-dashed rounded-md">
|
<div className="w-[64px] h-[64px] grid place-content-center border border-dashed bg-bg-input rounded-md">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<Upload />
|
<Plus />
|
||||||
<p>{t('common.upload')}</p>
|
<p>{t('common.upload')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -86,8 +90,8 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="margin-1 text-muted-foreground">
|
<div className="margin-1 text-text-secondary">
|
||||||
{t('knowledgeConfiguration.photoTip')}
|
{tips ?? t('knowledgeConfiguration.photoTip')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -106,8 +106,10 @@ const FormLabel = React.forwardRef<
|
|||||||
htmlFor={formItemId}
|
htmlFor={formItemId}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{required && <span className="text-destructive">*</span>}
|
<section>
|
||||||
{props.children}
|
{required && <span className="text-destructive">*</span>}
|
||||||
|
{props.children}
|
||||||
|
</section>
|
||||||
{tooltip && <FormTooltip tooltip={tooltip}></FormTooltip>}
|
{tooltip && <FormTooltip tooltip={tooltip}></FormTooltip>}
|
||||||
</Label>
|
</Label>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -140,6 +140,7 @@ const Modal: ModalType = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
|
disabled,
|
||||||
footer,
|
footer,
|
||||||
cancelText,
|
cancelText,
|
||||||
t,
|
t,
|
||||||
@ -158,7 +159,7 @@ const Modal: ModalType = ({
|
|||||||
onClick={() => maskClosable && onOpenChange?.(false)}
|
onClick={() => maskClosable && onOpenChange?.(false)}
|
||||||
>
|
>
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
className={`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-colors-background-neutral-standard rounded-lg shadow-lg border transition-all focus-visible:!outline-none`}
|
className={`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-bg-base rounded-lg shadow-lg border border-border-default transition-all focus-visible:!outline-none`}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* title */}
|
{/* title */}
|
||||||
|
|||||||
@ -137,7 +137,7 @@ export default {
|
|||||||
completed: 'Completed',
|
completed: 'Completed',
|
||||||
datasetLog: 'Dataset Log',
|
datasetLog: 'Dataset Log',
|
||||||
created: 'Created',
|
created: 'Created',
|
||||||
learnMore: 'Learn More',
|
learnMore: 'Built-in pipeline introduction',
|
||||||
general: 'General',
|
general: 'General',
|
||||||
chunkMethodTab: 'Chunk Method',
|
chunkMethodTab: 'Chunk Method',
|
||||||
testResults: 'Test Results',
|
testResults: 'Test Results',
|
||||||
@ -697,7 +697,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
|||||||
system: 'System',
|
system: 'System',
|
||||||
logout: 'Log out',
|
logout: 'Log out',
|
||||||
api: 'API',
|
api: 'API',
|
||||||
username: 'Username',
|
username: 'Name',
|
||||||
usernameMessage: 'Please input your username!',
|
usernameMessage: 'Please input your username!',
|
||||||
photo: 'Your photo',
|
photo: 'Your photo',
|
||||||
photoDescription: 'This will be displayed on your profile.',
|
photoDescription: 'This will be displayed on your profile.',
|
||||||
|
|||||||
@ -125,7 +125,7 @@ export default {
|
|||||||
completed: '已完成',
|
completed: '已完成',
|
||||||
datasetLog: '知识库日志',
|
datasetLog: '知识库日志',
|
||||||
created: '创建于',
|
created: '创建于',
|
||||||
learnMore: '了解更多',
|
learnMore: '内置pipeline简介',
|
||||||
general: '通用',
|
general: '通用',
|
||||||
chunkMethodTab: '切片方法',
|
chunkMethodTab: '切片方法',
|
||||||
testResults: '测试结果',
|
testResults: '测试结果',
|
||||||
|
|||||||
151
web/src/pages/profile-setting/profile/hooks/use-profile.ts
Normal file
151
web/src/pages/profile-setting/profile/hooks/use-profile.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// src/hooks/useProfile.ts
|
||||||
|
import {
|
||||||
|
useFetchUserInfo,
|
||||||
|
useSaveSetting,
|
||||||
|
} from '@/hooks/use-user-setting-request';
|
||||||
|
import { rsaPsw } from '@/utils';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface ProfileData {
|
||||||
|
userName: string;
|
||||||
|
timeZone: string;
|
||||||
|
currPasswd?: string;
|
||||||
|
newPasswd?: string;
|
||||||
|
avatar: string;
|
||||||
|
email: string;
|
||||||
|
confirmPasswd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditType = {
|
||||||
|
editName: 'editName',
|
||||||
|
editTimeZone: 'editTimeZone',
|
||||||
|
editPassword: 'editPassword',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type IEditType = keyof typeof EditType;
|
||||||
|
|
||||||
|
export const modalTitle = {
|
||||||
|
[EditType.editName]: 'Edit Name',
|
||||||
|
[EditType.editTimeZone]: 'Edit Time Zone',
|
||||||
|
[EditType.editPassword]: 'Edit Password',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const useProfile = () => {
|
||||||
|
const { data: userInfo } = useFetchUserInfo();
|
||||||
|
const [profile, setProfile] = useState<ProfileData>({
|
||||||
|
userName: '',
|
||||||
|
avatar: '',
|
||||||
|
timeZone: '',
|
||||||
|
email: '',
|
||||||
|
currPasswd: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [editType, setEditType] = useState<IEditType>(EditType.editName);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editForm, setEditForm] = useState<Partial<ProfileData>>({});
|
||||||
|
const {
|
||||||
|
saveSetting,
|
||||||
|
loading: submitLoading,
|
||||||
|
data: saveSettingData,
|
||||||
|
} = useSaveSetting();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// form.setValue('currPasswd', ''); // current password
|
||||||
|
const profile = {
|
||||||
|
userName: userInfo.nickname,
|
||||||
|
timeZone: userInfo.timezone,
|
||||||
|
avatar: userInfo.avatar || '',
|
||||||
|
email: userInfo.email,
|
||||||
|
currPasswd: userInfo.password,
|
||||||
|
};
|
||||||
|
setProfile(profile);
|
||||||
|
}, [userInfo, setProfile]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (saveSettingData === 0) {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditForm({});
|
||||||
|
}
|
||||||
|
}, [saveSettingData]);
|
||||||
|
const onSubmit = (newProfile: ProfileData) => {
|
||||||
|
const payload: Partial<{
|
||||||
|
nickname: string;
|
||||||
|
password: string;
|
||||||
|
new_password: string;
|
||||||
|
avatar: string;
|
||||||
|
timezone: string;
|
||||||
|
}> = {
|
||||||
|
nickname: newProfile.userName,
|
||||||
|
avatar: newProfile.avatar,
|
||||||
|
timezone: newProfile.timeZone,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
'currPasswd' in newProfile &&
|
||||||
|
'newPasswd' in newProfile &&
|
||||||
|
newProfile.currPasswd &&
|
||||||
|
newProfile.newPasswd
|
||||||
|
) {
|
||||||
|
payload.password = rsaPsw(newProfile.currPasswd!) as string;
|
||||||
|
payload.new_password = rsaPsw(newProfile.newPasswd!) as string;
|
||||||
|
}
|
||||||
|
console.log('payload', payload);
|
||||||
|
if (editType === EditType.editName && payload.nickname) {
|
||||||
|
saveSetting({ nickname: payload.nickname });
|
||||||
|
setProfile(newProfile);
|
||||||
|
}
|
||||||
|
if (editType === EditType.editTimeZone && payload.timezone) {
|
||||||
|
saveSetting({ timezone: payload.timezone });
|
||||||
|
setProfile(newProfile);
|
||||||
|
}
|
||||||
|
if (editType === EditType.editPassword && payload.password) {
|
||||||
|
saveSetting({
|
||||||
|
password: payload.password,
|
||||||
|
new_password: payload.new_password,
|
||||||
|
});
|
||||||
|
setProfile(newProfile);
|
||||||
|
}
|
||||||
|
// saveSetting(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = useCallback(
|
||||||
|
(type: IEditType) => {
|
||||||
|
setEditForm(profile);
|
||||||
|
setEditType(type);
|
||||||
|
setIsEditing(true);
|
||||||
|
},
|
||||||
|
[profile],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditForm({});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSave = (data: ProfileData) => {
|
||||||
|
console.log('handleSave', data);
|
||||||
|
const newProfile = { ...profile, ...data };
|
||||||
|
|
||||||
|
onSubmit(newProfile);
|
||||||
|
// setIsEditing(false);
|
||||||
|
// setEditForm({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarUpload = (avatar: string) => {
|
||||||
|
setProfile((prev) => ({ ...prev, avatar }));
|
||||||
|
saveSetting({ avatar });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile,
|
||||||
|
setProfile,
|
||||||
|
submitLoading: submitLoading,
|
||||||
|
isEditing,
|
||||||
|
editType,
|
||||||
|
editForm,
|
||||||
|
handleEditClick,
|
||||||
|
handleCancel,
|
||||||
|
handleSave,
|
||||||
|
handleAvatarUpload,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
|
// src/components/ProfilePage.tsx
|
||||||
|
import { AvatarUpload } from '@/components/avatar-upload';
|
||||||
import PasswordInput from '@/components/originui/password-input';
|
import PasswordInput from '@/components/originui/password-input';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -10,6 +11,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Modal } from '@/components/ui/modal/modal';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@ -18,434 +20,393 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { useTranslate } from '@/hooks/common-hooks';
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
import { useFetchUserInfo, useSaveSetting } from '@/hooks/user-setting-hooks';
|
|
||||||
import { TimezoneList } from '@/pages/user-setting/constants';
|
import { TimezoneList } from '@/pages/user-setting/constants';
|
||||||
import { rsaPsw } from '@/utils';
|
|
||||||
import { transformFile2Base64 } from '@/utils/file-util';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { TFunction } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { Loader2Icon, Pencil, Upload } from 'lucide-react';
|
import { Loader2Icon, PenLine } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { FC, useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
function defineSchema(
|
import { EditType, modalTitle, useProfile } from './hooks/use-profile';
|
||||||
t: TFunction<'translation', string>,
|
|
||||||
showPasswordForm = false,
|
|
||||||
) {
|
|
||||||
const baseSchema = z.object({
|
|
||||||
userName: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: t('usernameMessage') })
|
|
||||||
.trim(),
|
|
||||||
avatarUrl: z.string().trim(),
|
|
||||||
timeZone: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1, { message: t('timezonePlaceholder') }),
|
|
||||||
email: z
|
|
||||||
.string({ required_error: 'Please select an email to display.' })
|
|
||||||
.trim()
|
|
||||||
.regex(/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/, {
|
|
||||||
message: 'Enter a valid email address.',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (showPasswordForm) {
|
const baseSchema = z.object({
|
||||||
return baseSchema
|
userName: z
|
||||||
.extend({
|
.string()
|
||||||
currPasswd: z
|
.min(1, { message: t('setting.usernameMessage') })
|
||||||
.string({
|
.trim(),
|
||||||
required_error: t('currentPasswordMessage'),
|
timeZone: z
|
||||||
})
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1, { message: t('currentPasswordMessage') }),
|
.min(1, { message: t('setting.timezonePlaceholder') }),
|
||||||
newPasswd: z
|
});
|
||||||
.string({
|
|
||||||
required_error: t('confirmPasswordMessage'),
|
const nameSchema = baseSchema.extend({
|
||||||
})
|
currPasswd: z.string().optional(),
|
||||||
.trim()
|
newPasswd: z.string().optional(),
|
||||||
.min(8, { message: t('confirmPasswordMessage') }),
|
confirmPasswd: z.string().optional(),
|
||||||
confirmPasswd: z
|
});
|
||||||
.string({
|
|
||||||
required_error: t('newPasswordDescription'),
|
const passwordSchema = baseSchema
|
||||||
})
|
.extend({
|
||||||
.trim()
|
currPasswd: z
|
||||||
.min(8, { message: t('newPasswordDescription') }),
|
.string({
|
||||||
|
required_error: t('setting.currentPasswordMessage'),
|
||||||
})
|
})
|
||||||
.refine((data) => data.newPasswd === data.confirmPasswd, {
|
.trim(),
|
||||||
message: t('confirmPasswordNonMatchMessage'),
|
newPasswd: z
|
||||||
|
.string({
|
||||||
|
required_error: t('setting.newPasswordMessage'),
|
||||||
|
})
|
||||||
|
.trim()
|
||||||
|
.min(8, { message: t('setting.newPasswordDescription') }),
|
||||||
|
confirmPasswd: z
|
||||||
|
.string({
|
||||||
|
required_error: t('setting.confirmPasswordMessage'),
|
||||||
|
})
|
||||||
|
.trim()
|
||||||
|
.min(8, { message: t('setting.newPasswordDescription') }),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (
|
||||||
|
data.newPasswd &&
|
||||||
|
data.confirmPasswd &&
|
||||||
|
data.newPasswd !== data.confirmPasswd
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
path: ['confirmPasswd'],
|
path: ['confirmPasswd'],
|
||||||
|
message: t('setting.confirmPasswordNonMatchMessage'),
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return baseSchema;
|
const ProfilePage: FC = () => {
|
||||||
}
|
|
||||||
export default function Profile() {
|
|
||||||
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
|
||||||
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64
|
|
||||||
const { data: userInfo } = useFetchUserInfo();
|
|
||||||
const {
|
|
||||||
saveSetting,
|
|
||||||
loading: submitLoading,
|
|
||||||
data: saveUserData,
|
|
||||||
} = useSaveSetting();
|
|
||||||
|
|
||||||
const { t } = useTranslate('setting');
|
const { t } = useTranslate('setting');
|
||||||
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
|
||||||
const FormSchema = defineSchema(t, showPasswordForm);
|
const {
|
||||||
const form = useForm<z.infer<typeof FormSchema>>({
|
profile,
|
||||||
resolver: zodResolver(FormSchema),
|
editType,
|
||||||
|
isEditing,
|
||||||
|
submitLoading,
|
||||||
|
editForm,
|
||||||
|
handleEditClick,
|
||||||
|
handleCancel,
|
||||||
|
handleSave,
|
||||||
|
handleAvatarUpload,
|
||||||
|
} = useProfile();
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof baseSchema | typeof passwordSchema>>({
|
||||||
|
resolver: zodResolver(
|
||||||
|
editType === EditType.editPassword ? passwordSchema : nameSchema,
|
||||||
|
),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
userName: '',
|
userName: '',
|
||||||
avatarUrl: '',
|
|
||||||
timeZone: '',
|
timeZone: '',
|
||||||
email: '',
|
|
||||||
// currPasswd: '',
|
|
||||||
// newPasswd: '',
|
|
||||||
// confirmPasswd: '',
|
|
||||||
},
|
},
|
||||||
shouldUnregister: true,
|
// shouldUnregister: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// init user info when mounted
|
form.reset({ ...editForm, currPasswd: undefined });
|
||||||
form.setValue('email', userInfo?.email); // email
|
}, [editForm, form]);
|
||||||
form.setValue('userName', userInfo?.nickname); // nickname
|
|
||||||
form.setValue('timeZone', userInfo?.timezone); // time zone
|
|
||||||
// form.setValue('currPasswd', ''); // current password
|
|
||||||
setAvatarBase64Str(userInfo?.avatar ?? '');
|
|
||||||
}, [userInfo]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// const ModalContent: FC = () => {
|
||||||
if (saveUserData === 0) {
|
// // let content = null;
|
||||||
setShowPasswordForm(false);
|
// // if (editType === EditType.editName) {
|
||||||
form.resetField('currPasswd');
|
// // content = editName();
|
||||||
form.resetField('newPasswd');
|
// // }
|
||||||
form.resetField('confirmPasswd');
|
// return (
|
||||||
}
|
// <>
|
||||||
console.log('saveUserData', saveUserData);
|
|
||||||
}, [saveUserData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// </>
|
||||||
if (avatarFile) {
|
// );
|
||||||
// make use of img compression transformFile2Base64
|
// };
|
||||||
(async () => {
|
|
||||||
setAvatarBase64Str(await transformFile2Base64(avatarFile));
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
}, [avatarFile]);
|
|
||||||
|
|
||||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
|
||||||
const payload: Partial<{
|
|
||||||
nickname: string;
|
|
||||||
password: string;
|
|
||||||
new_password: string;
|
|
||||||
avatar: string;
|
|
||||||
timezone: string;
|
|
||||||
}> = {
|
|
||||||
nickname: data.userName,
|
|
||||||
avatar: avatarBase64Str,
|
|
||||||
timezone: data.timeZone,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (showPasswordForm && 'currPasswd' in data && 'newPasswd' in data) {
|
|
||||||
payload.password = rsaPsw(data.currPasswd!) as string;
|
|
||||||
payload.new_password = rsaPsw(data.newPasswd!) as string;
|
|
||||||
}
|
|
||||||
saveSetting(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showPasswordForm) {
|
|
||||||
form.register('currPasswd');
|
|
||||||
form.register('newPasswd');
|
|
||||||
form.register('confirmPasswd');
|
|
||||||
} else {
|
|
||||||
form.unregister(['currPasswd', 'newPasswd', 'confirmPasswd']);
|
|
||||||
}
|
|
||||||
}, [showPasswordForm]);
|
|
||||||
return (
|
return (
|
||||||
<section className="p-8">
|
<div className="min-h-screen bg-bg-base text-text-secondary p-5">
|
||||||
<h1 className="text-3xl font-bold">{t('profile')}</h1>
|
{/* Header */}
|
||||||
<div className="text-sm text-muted-foreground mb-6">
|
<header className="flex flex-col gap-1 justify-between items-start mb-6">
|
||||||
{t('profileDescription')}
|
<h1 className="text-2xl font-bold text-text-primary">{t('profile')}</h1>
|
||||||
</div>
|
<div className="text-sm text-text-secondary mb-6">
|
||||||
<div>
|
{t('profileDescription')}
|
||||||
<Form {...form}>
|
</div>
|
||||||
<form
|
</header>
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="block space-y-6"
|
|
||||||
>
|
|
||||||
{/* Username Field */}
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="userName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className=" items-center space-y-0 ">
|
|
||||||
<div className="flex w-[640px]">
|
|
||||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
|
||||||
<span className="text-red-600">*</span>
|
|
||||||
{t('username')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl className="w-3/4">
|
|
||||||
<Input placeholder="" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-[640px] pt-1">
|
|
||||||
<div className="w-1/4"></div>
|
|
||||||
<FormMessage />
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Avatar Field */}
|
{/* Main Content */}
|
||||||
<FormField
|
<div className="max-w-3xl space-y-11 w-3/4">
|
||||||
control={form.control}
|
{/* Name */}
|
||||||
name="avatarUrl"
|
<div className="flex items-start gap-4">
|
||||||
render={({ field }) => (
|
<label className="w-[190px] text-sm font-medium">
|
||||||
<FormItem className="flex items-center space-y-0">
|
{t('username')}
|
||||||
<div className="flex w-[640px]">
|
</label>
|
||||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
<div className="flex-1 flex items-center gap-4 min-w-60">
|
||||||
Avatar
|
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
|
||||||
</FormLabel>
|
{profile.userName}
|
||||||
<FormControl className="w-3/4">
|
</div>
|
||||||
<div className="flex justify-start items-end space-x-2">
|
<Button
|
||||||
<div className="relative group">
|
variant={'secondary'}
|
||||||
{!avatarBase64Str ? (
|
type="button"
|
||||||
<div className="w-[64px] h-[64px] grid place-content-center">
|
onClick={() => handleEditClick(EditType.editName)}
|
||||||
<div className="flex flex-col items-center">
|
className="text-sm text-text-secondary flex gap-1 px-1"
|
||||||
<Upload />
|
>
|
||||||
<p>Upload</p>
|
<PenLine size={12} /> Edit
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<div className="w-[64px] h-[64px] relative grid place-content-center">
|
|
||||||
<Avatar className="w-[64px] h-[64px] rounded-md">
|
{/* Avatar */}
|
||||||
<AvatarImage
|
<div className="flex items-start gap-4">
|
||||||
className="block"
|
<label className="w-[190px] text-sm font-medium">{t('avatar')}</label>
|
||||||
src={avatarBase64Str}
|
<div className="flex items-center gap-4">
|
||||||
alt=""
|
<AvatarUpload
|
||||||
/>
|
value={profile.avatar}
|
||||||
<AvatarFallback className="rounded-md"></AvatarFallback>
|
onChange={handleAvatarUpload}
|
||||||
</Avatar>
|
tips={'This will be displayed on your profile.'}
|
||||||
<div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60">
|
/>
|
||||||
<Pencil
|
</div>
|
||||||
size={16}
|
</div>
|
||||||
className="absolute right-1 bottom-1 opacity-50 hidden group-hover:block"
|
|
||||||
/>
|
{/* Time Zone */}
|
||||||
</div>
|
<div className="flex items-start gap-4">
|
||||||
</div>
|
<label className="w-[190px] text-sm font-medium">
|
||||||
)}
|
{t('timezone')}
|
||||||
|
</label>
|
||||||
|
<div className="flex-1 flex items-center gap-4">
|
||||||
|
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
|
||||||
|
{profile.timeZone}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={'secondary'}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleEditClick(EditType.editTimeZone)}
|
||||||
|
className="text-sm text-text-secondary flex gap-1 px-1"
|
||||||
|
>
|
||||||
|
<PenLine size={12} /> Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email Address */}
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<label className="w-[190px] text-sm font-medium"> {t('email')}</label>
|
||||||
|
<div className="flex-1 flex flex-col items-start gap-2">
|
||||||
|
<div className="text-sm text-text-primary flex-1 rounded-md py-1.5 ">
|
||||||
|
{profile.email}
|
||||||
|
</div>
|
||||||
|
<span className="text-text-secondary text-xs">
|
||||||
|
{t('emailDescription')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password */}
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<label className="w-[190px] text-sm font-medium">
|
||||||
|
{t('password')}
|
||||||
|
</label>
|
||||||
|
<div className="flex-1 flex items-center gap-4">
|
||||||
|
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
|
||||||
|
{profile.currPasswd ? '********' : ''}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={'secondary'}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleEditClick(EditType.editPassword)}
|
||||||
|
className="text-sm text-text-secondary flex gap-1 px-1"
|
||||||
|
>
|
||||||
|
<PenLine size={12} /> Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editType && (
|
||||||
|
<Modal
|
||||||
|
title={modalTitle[editType]}
|
||||||
|
open={isEditing}
|
||||||
|
showfooter={false}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="!w-[480px]"
|
||||||
|
>
|
||||||
|
{/* <ModalContent /> */}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit((data) => handleSave(data as any))}
|
||||||
|
className="flex flex-col mt-6 mb-8 ml-2 space-y-6 "
|
||||||
|
>
|
||||||
|
{editType === EditType.editName && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="userName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" items-center space-y-0 ">
|
||||||
|
<div className="flex flex-col w-full gap-2">
|
||||||
|
<FormLabel className="text-sm text-text-secondary whitespace-nowrap">
|
||||||
|
{t('username')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl className="w-full">
|
||||||
<Input
|
<Input
|
||||||
placeholder=""
|
placeholder=""
|
||||||
{...field}
|
{...field}
|
||||||
type="file"
|
className="bg-bg-input border-border-default"
|
||||||
title=""
|
|
||||||
accept="image/*"
|
|
||||||
className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
|
||||||
onChange={(ev) => {
|
|
||||||
const file = ev.target?.files?.[0];
|
|
||||||
if (
|
|
||||||
/\.(jpg|jpeg|png|webp|bmp)$/i.test(
|
|
||||||
file?.name ?? '',
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
setAvatarFile(file!);
|
|
||||||
}
|
|
||||||
ev.target.value = '';
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormControl>
|
||||||
<div className="margin-1 text-muted-foreground">
|
|
||||||
{t('avatarTip')}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
<div className="flex w-full pt-1">
|
||||||
</div>
|
<div className="w-1/4"></div>
|
||||||
<div className="flex w-[640px] pt-1">
|
<FormMessage />
|
||||||
<div className="w-1/4"></div>
|
</div>
|
||||||
<FormMessage />
|
</FormItem>
|
||||||
</div>
|
)}
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Time Zone Field */}
|
{editType === EditType.editTimeZone && (
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="timeZone"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="items-center space-y-0">
|
|
||||||
<div className="flex w-[640px]">
|
|
||||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
|
||||||
<span className="text-red-600">*</span>
|
|
||||||
{t('timezone')}
|
|
||||||
</FormLabel>
|
|
||||||
<Select onValueChange={field.onChange} value={field.value}>
|
|
||||||
<FormControl className="w-3/4">
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a timeZone" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{TimezoneList.map((timeStr) => (
|
|
||||||
<SelectItem key={timeStr} value={timeStr}>
|
|
||||||
{timeStr}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-[640px] pt-1">
|
|
||||||
<div className="w-1/4"></div>
|
|
||||||
<FormMessage />
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Email Address Field */}
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<div>
|
|
||||||
<FormItem className="items-center space-y-0">
|
|
||||||
<div className="flex w-[640px]">
|
|
||||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
|
||||||
{t('email')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl className="w-3/4">
|
|
||||||
<>{field.value}</>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-[640px] pt-1">
|
|
||||||
<div className="w-1/4"></div>
|
|
||||||
<FormMessage />
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
<div className="flex w-[640px] pt-1">
|
|
||||||
<p className="w-1/4"> </p>
|
|
||||||
<p className="text-sm text-muted-foreground whitespace-nowrap w-3/4">
|
|
||||||
{t('emailDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Password Section */}
|
|
||||||
<div className="pb-6">
|
|
||||||
<div className="flex items-center justify-start">
|
|
||||||
<h1 className="text-3xl font-bold">{t('password')}</h1>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="bg-transparent hover:bg-transparent border text-muted-foreground hover:text-white ml-10"
|
|
||||||
onClick={() => {
|
|
||||||
setShowPasswordForm(!showPasswordForm);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('changePassword')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{t('passwordDescription')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Password Form */}
|
|
||||||
{showPasswordForm && (
|
|
||||||
<>
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="currPasswd"
|
name="timeZone"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="items-center space-y-0">
|
<FormItem className="items-center space-y-0">
|
||||||
<div className="flex w-[640px]">
|
<div className="flex flex-col w-full gap-2">
|
||||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5">
|
<FormLabel className="text-sm text-text-secondary whitespace-nowrap">
|
||||||
<span className="text-red-600">*</span>
|
{t('timezone')}
|
||||||
{t('currentPassword')}
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl className="w-3/5">
|
<Select
|
||||||
<PasswordInput {...field} />
|
onValueChange={field.onChange}
|
||||||
</FormControl>
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl className="w-full bg-bg-input border-border-default">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a timeZone" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{TimezoneList.map((timeStr) => (
|
||||||
|
<SelectItem key={timeStr} value={timeStr}>
|
||||||
|
{timeStr}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-[640px] pt-1">
|
<div className="flex w-full pt-1">
|
||||||
<div className="min-w-[170px] max-w-[170px]"></div>
|
<div className="w-1/4"></div>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
)}
|
||||||
control={form.control}
|
|
||||||
name="newPasswd"
|
{editType === EditType.editPassword && (
|
||||||
render={({ field }) => (
|
<>
|
||||||
<FormItem className=" items-center space-y-0">
|
<FormField
|
||||||
<div className="flex w-[640px]">
|
control={form.control}
|
||||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5">
|
name="currPasswd"
|
||||||
<span className="text-red-600">*</span>
|
render={({ field }) => (
|
||||||
{t('newPassword')}
|
<FormItem className="items-center space-y-0">
|
||||||
</FormLabel>
|
<div className="flex flex-col w-full gap-2">
|
||||||
<FormControl className="w-3/5">
|
<FormLabel
|
||||||
<PasswordInput {...field} />
|
required
|
||||||
</FormControl>
|
className="text-sm flex justify-between text-text-secondary whitespace-nowrap"
|
||||||
</div>
|
>
|
||||||
<div className="flex w-[640px] pt-1">
|
{t('currentPassword')}
|
||||||
<div className="min-w-[170px] max-w-[170px]"></div>
|
</FormLabel>
|
||||||
<FormMessage />
|
<FormControl className="w-full">
|
||||||
</div>
|
<PasswordInput
|
||||||
</FormItem>
|
{...field}
|
||||||
)}
|
autoComplete="current-password"
|
||||||
/>
|
className="bg-bg-input border-border-default"
|
||||||
<FormField
|
/>
|
||||||
control={form.control}
|
</FormControl>
|
||||||
name="confirmPasswd"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className=" items-center space-y-0">
|
|
||||||
<div className="flex w-[640px]">
|
|
||||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5">
|
|
||||||
<span className="text-red-600">*</span>
|
|
||||||
{t('confirmPassword')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl className="w-3/5">
|
|
||||||
<PasswordInput
|
|
||||||
{...field}
|
|
||||||
onBlur={() => {
|
|
||||||
form.trigger('confirmPasswd');
|
|
||||||
}}
|
|
||||||
onChange={(ev) => {
|
|
||||||
form.setValue(
|
|
||||||
'confirmPasswd',
|
|
||||||
ev.target.value.trim(),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-[640px] pt-1">
|
|
||||||
<div className="min-w-[170px] max-w-[170px]">
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<FormMessage />
|
<div className="flex w-full pt-1">
|
||||||
</div>
|
<FormMessage />
|
||||||
</FormItem>
|
</div>
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
</>
|
/>
|
||||||
)}
|
<FormField
|
||||||
<div className="w-[640px] text-right space-x-4">
|
control={form.control}
|
||||||
<Button type="reset" variant="secondary">
|
name="newPasswd"
|
||||||
{t('cancel')}
|
render={({ field }) => (
|
||||||
</Button>
|
<FormItem className=" items-center space-y-0">
|
||||||
<Button type="submit" disabled={submitLoading}>
|
<div className="flex flex-col w-full gap-2">
|
||||||
{submitLoading && <Loader2Icon className="animate-spin" />}
|
<FormLabel
|
||||||
{t('save', { keyPrefix: 'common' })}
|
required
|
||||||
</Button>
|
className="text-sm text-text-secondary whitespace-nowrap"
|
||||||
</div>
|
>
|
||||||
</form>
|
{t('newPassword')}
|
||||||
</Form>
|
</FormLabel>
|
||||||
</div>
|
<FormControl className="w-full">
|
||||||
</section>
|
<PasswordInput
|
||||||
|
{...field}
|
||||||
|
autoComplete="new-password"
|
||||||
|
className="bg-bg-input border-border-default"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full pt-1">
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="confirmPasswd"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" items-center space-y-0">
|
||||||
|
<div className="flex flex-col w-full gap-2">
|
||||||
|
<FormLabel
|
||||||
|
required
|
||||||
|
className="text-sm text-text-secondary whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{t('confirmPassword')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl className="w-full">
|
||||||
|
<PasswordInput
|
||||||
|
{...field}
|
||||||
|
className="bg-bg-input border-border-default"
|
||||||
|
autoComplete="new-password"
|
||||||
|
onBlur={() => {
|
||||||
|
form.trigger('confirmPasswd');
|
||||||
|
}}
|
||||||
|
onChange={(ev) => {
|
||||||
|
form.setValue(
|
||||||
|
'confirmPasswd',
|
||||||
|
ev.target.value.trim(),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full pt-1">
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="w-full text-right space-x-4 !mt-11">
|
||||||
|
<Button type="reset" variant="secondary">
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={submitLoading}>
|
||||||
|
{submitLoading && <Loader2Icon className="animate-spin" />}
|
||||||
|
{t('save', { keyPrefix: 'common' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default ProfilePage;
|
||||||
|
|||||||
Reference in New Issue
Block a user