430 lines
14 KiB
TypeScript
430 lines
14 KiB
TypeScript
import { Input, Select, Upload } from '@/components/ui'
|
|
import Button from '@/components/ui/Button'
|
|
import { FormContainer, FormItem } 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 { Field, Form, Formik } from 'formik'
|
|
|
|
import { useCallback, 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,
|
|
FaHome,
|
|
FaUniversity,
|
|
} from 'react-icons/fa'
|
|
import * as Yup from 'yup'
|
|
import { ProfileDto, UpdateProfileDto } from '@/proxy/account/models'
|
|
import { getProfile, updateProfile } from '@/services/account.service'
|
|
import { CountryDto, getCountry } from '@/services/home.service'
|
|
import { SelectBoxOption } from '@/types/shared'
|
|
import { useSetting } from '@/utils/hooks/useSetting'
|
|
|
|
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 [countries, setCountries] = useState<CountryDto[]>([])
|
|
|
|
const auth = useStoreState((state) => state.auth)
|
|
const { setUser } = useStoreActions((actions) => actions.auth.user)
|
|
|
|
const { setting } = useSetting()
|
|
const isEmailUpdateEnabled = setting('Abp.Identity.User.IsEmailUpdateEnabled')
|
|
|
|
const { translate } = useLocalization()
|
|
const cropperRef = useRef<CropperRef>(null)
|
|
const previewRef = useRef<CropperPreviewRef>(null)
|
|
|
|
const fetchData = useCallback(async () => {
|
|
setLoading(true)
|
|
const [response, countryResponse] = await Promise.all([getProfile(), getCountry()])
|
|
setProfileData(response.data)
|
|
setCountries(countryResponse.data)
|
|
setImage(auth.user.avatar)
|
|
setFormData({
|
|
name: response.data.name,
|
|
surname: response.data.surname,
|
|
})
|
|
setLoading(false)
|
|
}, [auth.user.avatar])
|
|
|
|
useEffect(() => {
|
|
fetchData()
|
|
}, [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: 'bottom-end',
|
|
})
|
|
} else {
|
|
toast.push(<Notification title={resp?.error?.message} type="danger" />, {
|
|
placement: 'bottom-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)
|
|
}
|
|
})
|
|
|
|
const getProfileExtraValue = (key: string) => {
|
|
const extraProperties = profileData?.extraProperties
|
|
const value =
|
|
extraProperties?.[key] ?? extraProperties?.[`${key.charAt(0).toLowerCase()}${key.slice(1)}`]
|
|
|
|
return typeof value === 'string' || typeof value === 'number' ? String(value) : undefined
|
|
}
|
|
|
|
const getSelectValue = (options: SelectBoxOption[], value?: string) => {
|
|
if (!value) {
|
|
return null
|
|
}
|
|
|
|
return options.find((option) => option.value === value) ?? { value, label: value }
|
|
}
|
|
|
|
const nationalityOptions: SelectBoxOption[] = countries.map((country) => ({
|
|
value: country.name,
|
|
label: country.name,
|
|
}))
|
|
|
|
const educationOptions: SelectBoxOption[] = [
|
|
{
|
|
value: 'İlkokul',
|
|
label: translate('::App.EducationLevel.Primary') || 'İlkokul',
|
|
},
|
|
{
|
|
value: 'Ortaokul',
|
|
label: translate('::App.EducationLevel.MiddleSchool') || 'Ortaokul',
|
|
},
|
|
{
|
|
value: 'Lise',
|
|
label: translate('::App.EducationLevel.HighSchool') || 'Lise',
|
|
},
|
|
{
|
|
value: 'Ön Lisans',
|
|
label: translate('::App.EducationLevel.Associate') || 'Ön Lisans',
|
|
},
|
|
{
|
|
value: 'Lisans',
|
|
label: translate('::App.EducationLevel.Bachelor') || 'Lisans',
|
|
},
|
|
{
|
|
value: 'Yüksek Lisans',
|
|
label: translate('::App.EducationLevel.Master') || 'Yüksek Lisans',
|
|
},
|
|
{
|
|
value: 'Doktora',
|
|
label: translate('::App.EducationLevel.PhD') || 'Doktora',
|
|
},
|
|
]
|
|
|
|
const bloodTypeOptions: SelectBoxOption[] = [
|
|
{ value: 'A Rh+', label: 'A Rh+' },
|
|
{ value: 'A Rh-', label: 'A Rh-' },
|
|
{ value: 'B Rh+', label: 'B Rh+' },
|
|
{ value: 'B Rh-', label: 'B Rh-' },
|
|
{ value: 'AB Rh+', label: 'AB Rh+' },
|
|
{ value: 'AB Rh-', label: 'AB Rh-' },
|
|
{ value: '0 Rh+', label: '0 Rh+' },
|
|
{ value: '0 Rh-', label: '0 Rh-' },
|
|
]
|
|
|
|
if (loading) {
|
|
return <></>
|
|
}
|
|
|
|
return (
|
|
<Formik
|
|
enableReinitialize
|
|
initialValues={formData!}
|
|
validationSchema={schema}
|
|
onSubmit={(values, { setSubmitting }) => {
|
|
setSubmitting(true)
|
|
onFormSubmit(values, setSubmitting)
|
|
}}
|
|
>
|
|
{({ touched, errors, isSubmitting, resetForm }) => {
|
|
return (
|
|
<Form>
|
|
<FormContainer size="md">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 w-full">
|
|
<div>
|
|
<h6 className="mb-4">
|
|
{translate('::Abp.Identity.User.UserInformation.ContactInformation')}
|
|
</h6>
|
|
|
|
<FormItem label={translate('::Abp.Account.EmailAddress')}>
|
|
<Input
|
|
type="text"
|
|
disabled={isEmailUpdateEnabled?.toLowerCase() !== 'true'}
|
|
prefix={<FaEnvelope className="text-xl" />}
|
|
value={profileData?.email}
|
|
></Input>
|
|
</FormItem>
|
|
<FormItem
|
|
label={translate('::Abp.Identity.User.UserInformation.Name')}
|
|
invalid={(errors.name && touched.name) as boolean}
|
|
errorMessage={errors.name}
|
|
>
|
|
<Field
|
|
type="text"
|
|
autoComplete="off"
|
|
name="name"
|
|
placeholder="Name"
|
|
component={Input}
|
|
prefix={<FaUserCircle className="text-xl" />}
|
|
/>
|
|
</FormItem>
|
|
<FormItem
|
|
label={translate('::Abp.Identity.User.UserInformation.Surname')}
|
|
invalid={(errors.surname && touched.surname) as boolean}
|
|
errorMessage={errors.surname}
|
|
>
|
|
<Field
|
|
type="text"
|
|
autoComplete="off"
|
|
name="surname"
|
|
placeholder="Last Name"
|
|
component={Input}
|
|
prefix={<FaUserCircle className="text-xl" />}
|
|
/>
|
|
</FormItem>
|
|
<FormItem label={translate('::Abp.Identity.User.UserInformation.PhoneNumber')}>
|
|
<Input
|
|
disabled
|
|
type="text"
|
|
prefix={<FaPhone className="text-xl" />}
|
|
value={profileData?.phoneNumber}
|
|
></Input>
|
|
</FormItem>
|
|
<FormItem label={translate('::RocketUsername')}>
|
|
<Input
|
|
disabled
|
|
type="text"
|
|
prefix={<FaFacebookMessenger className="text-xl" />}
|
|
value={profileData?.extraProperties?.['RocketUsername'] as string | undefined}
|
|
></Input>
|
|
</FormItem>
|
|
</div>
|
|
|
|
<div>
|
|
<h6 className="mb-4">
|
|
{translate('::Abp.Identity.User.UserInformation.AdditionalInformation')}
|
|
</h6>
|
|
|
|
<FormItem label={translate('::Abp.Account.HomeAddress')}>
|
|
<Input
|
|
disabled
|
|
type="text"
|
|
prefix={<FaHome className="text-xl" />}
|
|
value={getProfileExtraValue('HomeAddress')}
|
|
></Input>
|
|
</FormItem>
|
|
<FormItem label={translate('::Abp.Account.Nationality')}>
|
|
<Select<SelectBoxOption>
|
|
isDisabled
|
|
options={nationalityOptions}
|
|
value={getSelectValue(
|
|
nationalityOptions,
|
|
getProfileExtraValue('Nationality'),
|
|
)}
|
|
/>
|
|
</FormItem>
|
|
<FormItem label={translate('::Abp.Account.EducationLevel')}>
|
|
<Select<SelectBoxOption>
|
|
isDisabled
|
|
options={educationOptions}
|
|
value={getSelectValue(
|
|
educationOptions,
|
|
getProfileExtraValue('EducationLevel'),
|
|
)}
|
|
/>
|
|
</FormItem>
|
|
<FormItem label={translate('::Abp.Account.GraduationSchool')}>
|
|
<Input
|
|
disabled
|
|
type="text"
|
|
prefix={<FaUniversity className="text-xl" />}
|
|
value={getProfileExtraValue('GraduationSchool')}
|
|
></Input>
|
|
</FormItem>
|
|
<FormItem label={translate('::Abp.Account.BloodType')}>
|
|
<Select<SelectBoxOption>
|
|
isDisabled
|
|
options={bloodTypeOptions}
|
|
value={getSelectValue(bloodTypeOptions, getProfileExtraValue('BloodType'))}
|
|
/>
|
|
</FormItem>
|
|
</div>
|
|
|
|
<div>
|
|
<h6 className="mb-4">Avatar</h6>
|
|
|
|
<FormItem>
|
|
<div className="flex flex-col gap-4">
|
|
{image ? (
|
|
<Cropper
|
|
ref={cropperRef}
|
|
src={image}
|
|
className="cropper max-h-[300px] max-w-[300px]"
|
|
stencilComponent={CircleStencil}
|
|
minHeight={100}
|
|
minWidth={100}
|
|
onUpdate={(cropper) => {
|
|
setTimeout(() => {
|
|
previewRef.current?.update(cropper)
|
|
}, 100)
|
|
}}
|
|
/>
|
|
) : (
|
|
<img
|
|
className="cropper max-h-[300px] max-w-[300px]"
|
|
src={auth.user.avatar}
|
|
/>
|
|
)}
|
|
<div className="flex items-start gap-4">
|
|
<div className="flex flex-col gap-2">
|
|
<Upload
|
|
className="cursor-pointer"
|
|
showList={false}
|
|
multiple={false}
|
|
uploadLimit={1}
|
|
beforeUpload={beforeUpload}
|
|
onChange={onChooseImage}
|
|
>
|
|
<Button
|
|
size="sm"
|
|
variant="default"
|
|
icon={<FaPlus />}
|
|
type="button"
|
|
></Button>
|
|
</Upload>
|
|
<Button
|
|
size="sm"
|
|
variant="solid"
|
|
type="button"
|
|
icon={<FaTrashAlt />}
|
|
onClick={() => setImage(undefined)}
|
|
></Button>
|
|
</div>
|
|
{image && (
|
|
<CropperPreview
|
|
ref={previewRef}
|
|
className="preview max-w-[100px] avatar-img avatar-circle"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</FormItem>
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 ltr:text-right">
|
|
<Button
|
|
size="sm"
|
|
className="ltr:mr-2 rtl:ml-2"
|
|
type="button"
|
|
onClick={() => resetForm()}
|
|
>
|
|
{translate('::Cancel')}
|
|
</Button>
|
|
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
|
|
{isSubmitting ? translate('::Saving') : translate('::Save')}
|
|
</Button>
|
|
</div>
|
|
</FormContainer>
|
|
</Form>
|
|
)
|
|
}}
|
|
</Formik>
|
|
)
|
|
}
|
|
|
|
export default General
|