ProfileImageSettings.tsx 6.2 KB

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