ProfileImageSettings.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import React, { useCallback, useState } from 'react';
  2. import { useTranslation } from 'next-i18next';
  3. import { apiPost, apiPostForm } from '~/client/util/apiv1-client';
  4. import { apiv3Put } from '~/client/util/apiv3-client';
  5. import { toastSuccess, toastError } from '~/client/util/toastr';
  6. import ImageCropModal from '~/components/Common/ImageCropModal';
  7. import { useCurrentUser } from '~/stores/context';
  8. import { generateGravatarSrc, GRAVATAR_DEFAULT } from '~/utils/gravatar';
  9. const DEFAULT_IMAGE = '/images/icons/user.svg';
  10. const ProfileImageSettings = (): JSX.Element => {
  11. const { t } = useTranslation();
  12. const { data: currentUser } = useCurrentUser();
  13. const [isGravatarEnabled, setGravatarEnabled] = useState(currentUser?.isGravatarEnabled);
  14. const [uploadedPictureSrc, setUploadedPictureSrc] = useState(() => {
  15. if (typeof currentUser?.imageAttachment === 'string') {
  16. return currentUser?.image;
  17. }
  18. return currentUser?.imageAttachment?.filePathProxied ?? currentUser?.image;
  19. });
  20. const [showImageCropModal, setShowImageCropModal] = useState(false);
  21. const [imageCropSrc, setImageCropSrc] = useState<string|ArrayBuffer|null>(null);
  22. const selectFileHandler = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
  23. if (e.target.files == null || e.target.files.length === 0) {
  24. return;
  25. }
  26. const reader = new FileReader();
  27. reader.addEventListener('load', () => setImageCropSrc(reader.result));
  28. reader.readAsDataURL(e.target.files[0]);
  29. setShowImageCropModal(true);
  30. }, []);
  31. const processImageCompletedHandler = useCallback(async(croppedImage) => {
  32. try {
  33. const formData = new FormData();
  34. formData.append('file', croppedImage);
  35. const response = await apiPostForm('/attachments.uploadProfileImage', formData);
  36. toastSuccess(t('toaster.update_successed', { target: t('Current Image'), ns: 'commons' }));
  37. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  38. setUploadedPictureSrc((response as any).attachment.filePathProxied);
  39. }
  40. catch (err) {
  41. toastError(err);
  42. }
  43. }, [t]);
  44. const deleteImageHandler = useCallback(async() => {
  45. try {
  46. await apiPost('/attachments.removeProfileImage');
  47. setUploadedPictureSrc(undefined);
  48. toastSuccess(t('toaster.update_successed', { target: t('Current Image'), ns: 'commons' }));
  49. }
  50. catch (err) {
  51. toastError(err);
  52. }
  53. }, [t]);
  54. const submit = useCallback(async() => {
  55. try {
  56. const response = await apiv3Put('/personal-setting/image-type', { isGravatarEnabled });
  57. const { userData } = response.data;
  58. setGravatarEnabled(userData.isGravatarEnabled);
  59. toastSuccess(t('toaster.update_successed', { target: t('Set Profile Image'), ns: 'commons' }));
  60. }
  61. catch (err) {
  62. toastError(err);
  63. }
  64. }, [isGravatarEnabled, t]);
  65. if (currentUser == null) {
  66. return <></>;
  67. }
  68. return (
  69. <>
  70. <div className="row justify-content-around mt-5 mt-md-4">
  71. <div className="col-md-3">
  72. <h5>
  73. <div className="form-check radio-primary">
  74. <input
  75. type="radio"
  76. id="radioGravatar"
  77. className="form-check-input"
  78. form="formImageType"
  79. name="imagetypeForm[isGravatarEnabled]"
  80. checked={isGravatarEnabled}
  81. onChange={() => setGravatarEnabled(true)}
  82. />
  83. <label className="form-label form-check-label" htmlFor="radioGravatar">
  84. <img src={GRAVATAR_DEFAULT} className="me-1" data-vrt-blackout-profile /> Gravatar
  85. </label>
  86. <a href="https://gravatar.com/" target="_blank" rel="noopener noreferrer">
  87. <small><span className="material-symbols-outlined ms-2 text-secondary" aria-hidden="true">info</span></small>
  88. </a>
  89. </div>
  90. </h5>
  91. <img src={generateGravatarSrc(currentUser.email)} className="rounded-pill" width="64" data-vrt-blackout-profile />
  92. </div>
  93. <div className="col-md-7 mt-5">
  94. <h5>
  95. <div className="form-check radio-primary">
  96. <input
  97. type="radio"
  98. id="radioUploadPicture"
  99. className="form-check-input"
  100. form="formImageType"
  101. name="imagetypeForm[isGravatarEnabled]"
  102. checked={!isGravatarEnabled}
  103. onChange={() => setGravatarEnabled(false)}
  104. />
  105. <label className="form-label form-check-label" htmlFor="radioUploadPicture">
  106. { t('Upload Image') }
  107. </label>
  108. </div>
  109. </h5>
  110. <div className="row mt-3">
  111. <label className="col-md-6 col-lg-4 col-form-label text-start">
  112. { t('Current Image') }
  113. </label>
  114. <div className="col-md-6 col-lg-8">
  115. <p className="mb-0"><img src={uploadedPictureSrc ?? DEFAULT_IMAGE} className="picture picture-lg rounded-circle" id="settingUserPicture" /></p>
  116. {uploadedPictureSrc && <button type="button" className="btn btn-danger" onClick={deleteImageHandler}>{ t('Delete Image') }</button>}
  117. </div>
  118. </div>
  119. <div className="row align-items-center mt-3 mt-md-5">
  120. <label className="col-md-6 col-lg-4 col-form-label text-start mt-3 mt-md-0">
  121. {t('Upload new image')}
  122. </label>
  123. <div className="col-md-6 col-lg-8">
  124. <input type="file" onChange={selectFileHandler} name="profileImage" accept="image/*" />
  125. </div>
  126. </div>
  127. </div>
  128. </div>
  129. <ImageCropModal
  130. isShow={showImageCropModal}
  131. src={imageCropSrc}
  132. onModalClose={() => setShowImageCropModal(false)}
  133. onImageProcessCompleted={processImageCompletedHandler}
  134. isCircular
  135. showCropOption
  136. />
  137. <div className="row mt-4">
  138. <div className="offset-4 col-5">
  139. <button type="button" className="btn btn-primary" onClick={submit}>
  140. {t('Update')}
  141. </button>
  142. </div>
  143. </div>
  144. </>
  145. );
  146. };
  147. export default ProfileImageSettings;