sozsoft-platform/ui/src/views/admin/profile/components/General.tsx

313 lines
10 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
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