313 lines
10 KiB
TypeScript
313 lines
10 KiB
TypeScript
|
|
import { Input, Upload } from '@/components/ui'
|
||
|
|
import Button from '@/components/ui/Button'
|
||
|
|
import { FormContainer } from '@/components/ui/Form'
|
||
|
|
import Notification from '@/components/ui/Notification'
|
||
|
|
import toast from '@/components/ui/toast'
|
||
|
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||
|
|
import { useStoreActions, useStoreState } from '@/store'
|
||
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
|
|
import dayjs from 'dayjs'
|
||
|
|
import type { FieldProps } from 'formik'
|
||
|
|
import { Field, Form, Formik } from 'formik'
|
||
|
|
|
||
|
|
import { useEffect, useRef, useState } from 'react'
|
||
|
|
import {
|
||
|
|
CircleStencil,
|
||
|
|
Cropper,
|
||
|
|
CropperPreview,
|
||
|
|
CropperPreviewRef,
|
||
|
|
CropperRef,
|
||
|
|
} from 'react-advanced-cropper'
|
||
|
|
import 'react-advanced-cropper/dist/style.css'
|
||
|
|
import {
|
||
|
|
FaFacebookMessenger,
|
||
|
|
FaEnvelope,
|
||
|
|
FaTrashAlt,
|
||
|
|
FaUserCircle,
|
||
|
|
FaPhone,
|
||
|
|
FaPlus,
|
||
|
|
} from 'react-icons/fa'
|
||
|
|
import * as Yup from 'yup'
|
||
|
|
import isEmpty from 'lodash/isEmpty'
|
||
|
|
import FormRow from '@/views/shared/FormRow'
|
||
|
|
import FormDesription from '@/views/shared/FormDesription'
|
||
|
|
import { ProfileDto, UpdateProfileDto } from '@/proxy/account/models'
|
||
|
|
import { getProfile, updateProfile } from '@/services/account.service'
|
||
|
|
|
||
|
|
const schema = Yup.object().shape({
|
||
|
|
name: Yup.string().min(3).max(50).required(),
|
||
|
|
surname: Yup.string().min(3).max(50).required(),
|
||
|
|
})
|
||
|
|
|
||
|
|
const General = () => {
|
||
|
|
const [loading, setLoading] = useState(true)
|
||
|
|
const [profileData, setProfileData] = useState<ProfileDto>()
|
||
|
|
const [formData, setFormData] = useState<UpdateProfileDto>()
|
||
|
|
const [image, setImage] = useState<string | undefined>()
|
||
|
|
|
||
|
|
const auth = useStoreState((state) => state.auth)
|
||
|
|
const { setUser } = useStoreActions((actions) => actions.auth.user)
|
||
|
|
|
||
|
|
const { translate } = useLocalization()
|
||
|
|
const cropperRef = useRef<CropperRef>(null)
|
||
|
|
const previewRef = useRef<CropperPreviewRef>(null)
|
||
|
|
|
||
|
|
const fetchData = async () => {
|
||
|
|
setLoading(true)
|
||
|
|
const response = await getProfile()
|
||
|
|
setProfileData(response.data)
|
||
|
|
setImage(auth.user.avatar)
|
||
|
|
setFormData({
|
||
|
|
name: response.data.name,
|
||
|
|
surname: response.data.surname,
|
||
|
|
})
|
||
|
|
setLoading(false)
|
||
|
|
}
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isEmpty(profileData)) {
|
||
|
|
fetchData()
|
||
|
|
}
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
const onChooseImage = async (file: File[]) => {
|
||
|
|
if (file[0]) {
|
||
|
|
setImage(URL.createObjectURL(file[0]))
|
||
|
|
} else {
|
||
|
|
setImage(undefined)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const beforeUpload = (files: FileList | null, fileList: File[]) => {
|
||
|
|
let valid: string | boolean = true
|
||
|
|
|
||
|
|
const allowedFileType = ['image/jpeg', 'image/png', 'image/gif']
|
||
|
|
const maxFileSize = 2000000
|
||
|
|
|
||
|
|
if (fileList.length >= 1) {
|
||
|
|
return `Sadece bir dosya seçebilirsiniz`
|
||
|
|
}
|
||
|
|
|
||
|
|
if (files) {
|
||
|
|
for (const f of files) {
|
||
|
|
if (!allowedFileType.includes(f.type)) {
|
||
|
|
valid = '.jpg, .jpeg, .gif veya .png yükleyebilirsiniz'
|
||
|
|
} else if (f.size >= maxFileSize) {
|
||
|
|
valid = 'En fazla 2mb dosya yükleyebilirsiniz'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return valid
|
||
|
|
}
|
||
|
|
|
||
|
|
const onFormSubmit = async (
|
||
|
|
values: UpdateProfileDto,
|
||
|
|
setSubmitting: (isSubmitting: boolean) => void,
|
||
|
|
) => {
|
||
|
|
const avatar = await getCroppedAvatar()
|
||
|
|
const resp = await updateProfile({
|
||
|
|
name: values.name,
|
||
|
|
surname: values.surname,
|
||
|
|
avatar: avatar ? new File([avatar], 'avatar') : undefined,
|
||
|
|
})
|
||
|
|
if (resp.status === 200) {
|
||
|
|
setUser({
|
||
|
|
...auth.user,
|
||
|
|
name: `${resp.data.name} ${resp.data.surname}`.trim(),
|
||
|
|
avatar: AVATAR_URL(auth.user.id, auth.tenant.tenantId) + `?${dayjs().unix()}`,
|
||
|
|
})
|
||
|
|
|
||
|
|
toast.push(<Notification title={'Profil güncellendi'} type="success" />, {
|
||
|
|
placement: 'top-end',
|
||
|
|
})
|
||
|
|
} else {
|
||
|
|
toast.push(<Notification title={resp?.error?.message} type="danger" />, {
|
||
|
|
placement: 'top-end',
|
||
|
|
})
|
||
|
|
}
|
||
|
|
setSubmitting(false)
|
||
|
|
}
|
||
|
|
|
||
|
|
const getCroppedAvatar = () =>
|
||
|
|
new Promise<Blob | null>((resolve, reject) => {
|
||
|
|
const canvas = cropperRef.current?.getCanvas({
|
||
|
|
minHeight: 100,
|
||
|
|
minWidth: 100,
|
||
|
|
maxHeight: 300,
|
||
|
|
maxWidth: 300,
|
||
|
|
})
|
||
|
|
if (canvas) {
|
||
|
|
canvas.toBlob((blob) => {
|
||
|
|
if (blob) {
|
||
|
|
resolve(blob)
|
||
|
|
} else {
|
||
|
|
reject()
|
||
|
|
}
|
||
|
|
}, 'image/jpeg')
|
||
|
|
} else {
|
||
|
|
resolve(null)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return <></>
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Formik
|
||
|
|
enableReinitialize
|
||
|
|
initialValues={formData!}
|
||
|
|
validationSchema={schema}
|
||
|
|
onSubmit={(values, { setSubmitting }) => {
|
||
|
|
setSubmitting(true)
|
||
|
|
onFormSubmit(values, setSubmitting)
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{({ touched, errors, isSubmitting, resetForm }) => {
|
||
|
|
const validatorProps = { touched, errors }
|
||
|
|
return (
|
||
|
|
<Form>
|
||
|
|
<FormContainer>
|
||
|
|
<FormDesription
|
||
|
|
title={translate('::Abp.Identity.Profile.General')}
|
||
|
|
desc={translate('::Abp.Identity.Profile.General.Description')}
|
||
|
|
/>
|
||
|
|
<FormRow
|
||
|
|
name="email"
|
||
|
|
label={translate('::Abp.Account.EmailAddress')}
|
||
|
|
{...validatorProps}
|
||
|
|
>
|
||
|
|
<Input
|
||
|
|
type="text"
|
||
|
|
disabled
|
||
|
|
prefix={<FaEnvelope className="text-xl" />}
|
||
|
|
value={profileData?.email}
|
||
|
|
></Input>
|
||
|
|
</FormRow>
|
||
|
|
<FormRow
|
||
|
|
name="phoneNumber"
|
||
|
|
label={translate('::Abp.Identity.User.UserInformation.PhoneNumber')}
|
||
|
|
{...validatorProps}
|
||
|
|
>
|
||
|
|
<Input
|
||
|
|
type="text"
|
||
|
|
disabled
|
||
|
|
prefix={<FaPhone className="text-xl" />}
|
||
|
|
value={profileData?.phoneNumber}
|
||
|
|
></Input>
|
||
|
|
</FormRow>
|
||
|
|
<FormRow name="phoneNumber" label={translate('::RocketUsername')} {...validatorProps}>
|
||
|
|
<Input
|
||
|
|
type="text"
|
||
|
|
disabled
|
||
|
|
prefix={<FaFacebookMessenger className="text-xl" />}
|
||
|
|
value={profileData?.extraProperties?.['RocketUsername'] as string | undefined}
|
||
|
|
></Input>
|
||
|
|
</FormRow>
|
||
|
|
<FormRow
|
||
|
|
name="name"
|
||
|
|
label={translate('::Abp.Identity.User.UserInformation.Name')}
|
||
|
|
{...validatorProps}
|
||
|
|
>
|
||
|
|
<Field
|
||
|
|
type="text"
|
||
|
|
autoComplete="off"
|
||
|
|
name="name"
|
||
|
|
placeholder="Name"
|
||
|
|
component={Input}
|
||
|
|
prefix={<FaUserCircle className="text-xl" />}
|
||
|
|
/>
|
||
|
|
</FormRow>
|
||
|
|
<FormRow
|
||
|
|
name="surname"
|
||
|
|
label={translate('::Abp.Identity.User.UserInformation.Surname')}
|
||
|
|
{...validatorProps}
|
||
|
|
>
|
||
|
|
<Field
|
||
|
|
type="text"
|
||
|
|
autoComplete="off"
|
||
|
|
name="surname"
|
||
|
|
placeholder="Last Name"
|
||
|
|
component={Input}
|
||
|
|
prefix={<FaUserCircle className="text-xl" />}
|
||
|
|
/>
|
||
|
|
</FormRow>
|
||
|
|
<FormRow name="avatar" label="Avatar" alignCenter={false} {...validatorProps}>
|
||
|
|
<Field name="avatar">
|
||
|
|
{({ field, form }: FieldProps) => {
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<div>
|
||
|
|
<div className="flex flex-col lg:flex-row items-start gap-5">
|
||
|
|
{image ? (
|
||
|
|
<Cropper
|
||
|
|
ref={cropperRef}
|
||
|
|
src={image}
|
||
|
|
onUpdate={(cropper) => {
|
||
|
|
setTimeout(() => {
|
||
|
|
previewRef.current?.update(cropper)
|
||
|
|
}, 100)
|
||
|
|
}}
|
||
|
|
className="cropper max-h-[300px] max-w-[300px]"
|
||
|
|
stencilComponent={CircleStencil}
|
||
|
|
minHeight={100}
|
||
|
|
minWidth={100}
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<img
|
||
|
|
className="cropper max-h-[300px] max-w-[300px]"
|
||
|
|
src={auth.user.avatar}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
<div className="flex flex-row gap-4">
|
||
|
|
<div className="flex flex-col">
|
||
|
|
<Upload
|
||
|
|
className="cursor-pointer"
|
||
|
|
showList={false}
|
||
|
|
multiple={false}
|
||
|
|
uploadLimit={1}
|
||
|
|
beforeUpload={beforeUpload}
|
||
|
|
onChange={onChooseImage}
|
||
|
|
>
|
||
|
|
<Button icon={<FaPlus />} type="button"></Button>
|
||
|
|
</Upload>
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
className="my-2"
|
||
|
|
icon={<FaTrashAlt />}
|
||
|
|
onClick={() => setImage(undefined)}
|
||
|
|
></Button>
|
||
|
|
</div>
|
||
|
|
{image && (
|
||
|
|
<CropperPreview
|
||
|
|
ref={previewRef}
|
||
|
|
className="preview max-w-[100px] avatar-img avatar-circle border border-gray-400"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
)
|
||
|
|
}}
|
||
|
|
</Field>
|
||
|
|
</FormRow>
|
||
|
|
<div className="mt-4 ltr:text-right">
|
||
|
|
<Button className="ltr:mr-2 rtl:ml-2" type="button" onClick={() => resetForm()}>
|
||
|
|
{translate('::Cancel')}
|
||
|
|
</Button>
|
||
|
|
<Button variant="solid" loading={isSubmitting} type="submit">
|
||
|
|
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</FormContainer>
|
||
|
|
</Form>
|
||
|
|
)
|
||
|
|
}}
|
||
|
|
</Formik>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default General
|