ProfileImageSettings.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import React, { type JSX, 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 { toastError, toastSuccess } from '~/client/util/toastr';
  8. import { useCurrentUser } from '~/states/global';
  9. import { GRAVATAR_DEFAULT, generateGravatarSrc } from '~/utils/gravatar';
  10. const DEFAULT_IMAGE = '/images/icons/user.svg';
  11. const ProfileImageSettings = (): JSX.Element => {
  12. const { t } = useTranslation();
  13. const currentUser = useCurrentUser();
  14. const [isGravatarEnabled, setGravatarEnabled] = useState(
  15. currentUser?.isGravatarEnabled,
  16. );
  17. const [uploadedPictureSrc, setUploadedPictureSrc] = useState(() => {
  18. if (
  19. currentUser?.imageAttachment != null &&
  20. isPopulated(currentUser.imageAttachment)
  21. ) {
  22. return currentUser.imageAttachment.filePathProxied ?? currentUser.image;
  23. }
  24. return currentUser?.image;
  25. });
  26. const [showImageCropModal, setShowImageCropModal] = useState(false);
  27. const [imageCropSrc, setImageCropSrc] = useState<string | ArrayBuffer | null>(
  28. null,
  29. );
  30. const selectFileHandler = useCallback(
  31. (e: React.ChangeEvent<HTMLInputElement>) => {
  32. if (e.target.files == null || e.target.files.length === 0) {
  33. return;
  34. }
  35. const reader = new FileReader();
  36. reader.addEventListener('load', () => setImageCropSrc(reader.result));
  37. reader.readAsDataURL(e.target.files[0]);
  38. setShowImageCropModal(true);
  39. },
  40. [],
  41. );
  42. const processImageCompletedHandler = useCallback(
  43. async (croppedImage) => {
  44. try {
  45. const formData = new FormData();
  46. formData.append('file', croppedImage);
  47. const response = await apiPostForm(
  48. '/attachments.uploadProfileImage',
  49. formData,
  50. );
  51. toastSuccess(
  52. t('toaster.update_successed', {
  53. target: t('Current Image'),
  54. ns: 'commons',
  55. }),
  56. );
  57. setUploadedPictureSrc((response as any).attachment.filePathProxied);
  58. } catch (err) {
  59. toastError(err);
  60. }
  61. },
  62. [t],
  63. );
  64. const deleteImageHandler = useCallback(async () => {
  65. try {
  66. await apiPost('/attachments.removeProfileImage');
  67. setUploadedPictureSrc(undefined);
  68. toastSuccess(
  69. t('toaster.update_successed', {
  70. target: t('Current Image'),
  71. ns: 'commons',
  72. }),
  73. );
  74. } catch (err) {
  75. toastError(err);
  76. }
  77. }, [t]);
  78. const submit = useCallback(async () => {
  79. try {
  80. const response = await apiv3Put('/personal-setting/image-type', {
  81. isGravatarEnabled,
  82. });
  83. const { userData } = response.data;
  84. setGravatarEnabled(userData.isGravatarEnabled);
  85. toastSuccess(
  86. t('toaster.update_successed', {
  87. target: t('Set Profile Image'),
  88. ns: 'commons',
  89. }),
  90. );
  91. } catch (err) {
  92. toastError(err);
  93. }
  94. }, [isGravatarEnabled, t]);
  95. if (currentUser == null) {
  96. return <></>;
  97. }
  98. return (
  99. <>
  100. <div className="row justify-content-around mt-5 mt-md-4">
  101. <div className="col-md-3">
  102. <h5>
  103. <div className="form-check radio-primary">
  104. <input
  105. type="radio"
  106. id="radioGravatar"
  107. className="form-check-input"
  108. form="formImageType"
  109. name="imagetypeForm[isGravatarEnabled]"
  110. checked={isGravatarEnabled}
  111. onChange={() => setGravatarEnabled(true)}
  112. />
  113. <label
  114. className="form-label form-check-label"
  115. htmlFor="radioGravatar"
  116. >
  117. <img
  118. src={GRAVATAR_DEFAULT}
  119. alt="Gravatar"
  120. className="me-1"
  121. data-vrt-blackout-profile
  122. />{' '}
  123. Gravatar
  124. </label>
  125. <a
  126. href="https://gravatar.com/"
  127. target="_blank"
  128. rel="noopener noreferrer"
  129. >
  130. <small>
  131. <span
  132. className="material-symbols-outlined ms-2 text-secondary"
  133. aria-hidden="true"
  134. >
  135. info
  136. </span>
  137. </small>
  138. </a>
  139. </div>
  140. </h5>
  141. <img
  142. src={generateGravatarSrc(currentUser.email)}
  143. alt="Gravatar"
  144. className="rounded-pill"
  145. width="64"
  146. height="64"
  147. data-vrt-blackout-profile
  148. />
  149. </div>
  150. <div className="col-md-7 mt-5 mt-md-0">
  151. <h5>
  152. <div className="form-check radio-primary">
  153. <input
  154. type="radio"
  155. id="radioUploadPicture"
  156. className="form-check-input"
  157. form="formImageType"
  158. name="imagetypeForm[isGravatarEnabled]"
  159. checked={!isGravatarEnabled}
  160. onChange={() => setGravatarEnabled(false)}
  161. />
  162. <label
  163. className="form-label form-check-label"
  164. htmlFor="radioUploadPicture"
  165. >
  166. {t('Upload Image')}
  167. </label>
  168. </div>
  169. </h5>
  170. <div className="row mt-3">
  171. <span className="col-md-6 col-lg-4 col-form-label text-start">
  172. {t('Current Image')}
  173. </span>
  174. <div className="col-md-6 col-lg-8">
  175. <p className="mb-0">
  176. <img
  177. src={uploadedPictureSrc ?? DEFAULT_IMAGE}
  178. alt={t('Current Image')}
  179. width="64"
  180. height="64"
  181. className="rounded-circle"
  182. id="settingUserPicture"
  183. />
  184. </p>
  185. {uploadedPictureSrc && (
  186. <button
  187. type="button"
  188. className="btn btn-danger mt-2"
  189. onClick={deleteImageHandler}
  190. >
  191. {t('Delete Image')}
  192. </button>
  193. )}
  194. </div>
  195. </div>
  196. <div className="row align-items-center mt-3 mt-md-5">
  197. <span className="col-md-6 col-lg-4 col-form-label text-start mt-3 mt-md-0">
  198. {t('Upload new image')}
  199. </span>
  200. <div className="col-md-6 col-lg-8">
  201. <input
  202. type="file"
  203. onChange={selectFileHandler}
  204. name="profileImage"
  205. accept="image/png,image/jpeg,image/jpg,image/gif,image/webp,image/avif,image/heic,image/heif,image/tiff,image/svg+xml"
  206. />
  207. </div>
  208. </div>
  209. </div>
  210. </div>
  211. <ImageCropModal
  212. isShow={showImageCropModal}
  213. src={imageCropSrc}
  214. onModalClose={() => setShowImageCropModal(false)}
  215. onImageProcessCompleted={processImageCompletedHandler}
  216. isCircular
  217. showCropOption
  218. />
  219. <div className="row mt-4">
  220. <div className="offset-4 col-5">
  221. <button type="button" className="btn btn-primary" onClick={submit}>
  222. {t('Update')}
  223. </button>
  224. </div>
  225. </div>
  226. </>
  227. );
  228. };
  229. export default ProfileImageSettings;