CustomizeLogoSetting.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import React, {
  2. useCallback, useState, useRef, type JSX,
  3. } from 'react';
  4. import { useAtomValue, useSetAtom } from 'jotai';
  5. import { useTranslation } from 'react-i18next';
  6. import ImageCropModal from '~/client/components/Common/ImageCropModal';
  7. import {
  8. apiv3Delete, apiv3PostForm, apiv3Put,
  9. } from '~/client/util/apiv3-client';
  10. import { toastError, toastSuccess } from '~/client/util/toastr';
  11. import { useIsDefaultLogo } from '~/states/global';
  12. import { isCustomizedLogoUploadedAtom } from '~/states/server-configurations';
  13. import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
  14. const DEFAULT_LOGO = '/images/logo.svg';
  15. const CUSTOMIZED_LOGO = '/attachment/brand-logo';
  16. const CustomizeLogoSetting = (): JSX.Element => {
  17. const { t } = useTranslation();
  18. const isDefaultLogo = useIsDefaultLogo();
  19. const isCustomizedLogoUploaded = useAtomValue(isCustomizedLogoUploadedAtom);
  20. const setIsCustomizedLogoUploaded = useSetAtom(isCustomizedLogoUploadedAtom);
  21. const [uploadLogoSrc, setUploadLogoSrc] = useState<ArrayBuffer | string | null>(null);
  22. const [isImageCropModalShow, setIsImageCropModalShow] = useState<boolean>(false);
  23. const [isDefaultLogoSelected, setIsDefaultLogoSelected] = useState<boolean>(isDefaultLogo ?? true);
  24. const [retrieveError, setRetrieveError] = useState<any>();
  25. const fileInputRef = useRef<HTMLInputElement | null>(null);
  26. const isUpdateButtonDisabled = retrieveError != null || (!isDefaultLogoSelected && uploadLogoSrc == null && !isCustomizedLogoUploaded);
  27. const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
  28. const files = e.target.files;
  29. const hasFile = files != null && files.length > 0;
  30. if (hasFile) {
  31. const reader = new FileReader();
  32. reader.addEventListener('load', () => setUploadLogoSrc(reader.result));
  33. reader.readAsDataURL(files[0]);
  34. setIsImageCropModalShow(true);
  35. }
  36. }, [setUploadLogoSrc, setIsImageCropModalShow]);
  37. const onClickSubmit = useCallback(async () => {
  38. try {
  39. await apiv3Put('/customize-setting/customize-logo', { isDefaultLogo: isDefaultLogoSelected });
  40. toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
  41. }
  42. catch (err) {
  43. toastError(err);
  44. }
  45. }, [t, isDefaultLogoSelected]);
  46. const clearFileInput = useCallback(() => {
  47. if (fileInputRef.current) {
  48. fileInputRef.current.value = '';
  49. }
  50. }, []);
  51. const onClickDeleteBtn = useCallback(async () => {
  52. try {
  53. await apiv3Delete('/customize-setting/delete-brand-logo');
  54. setIsCustomizedLogoUploaded(false);
  55. toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
  56. setUploadLogoSrc(null);
  57. setIsImageCropModalShow(false);
  58. clearFileInput();
  59. }
  60. catch (err) {
  61. toastError(err);
  62. setRetrieveError(err);
  63. throw new Error('Failed to delete logo');
  64. }
  65. }, [setIsCustomizedLogoUploaded, t, setUploadLogoSrc, setIsImageCropModalShow, clearFileInput, setRetrieveError]);
  66. const processImageCompletedHandler = useCallback(async (croppedImage) => {
  67. try {
  68. const formData = new FormData();
  69. formData.append('file', croppedImage);
  70. const response = await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
  71. const newLogoUrl = response.data.attachment.filePathProxied;
  72. setUploadLogoSrc(newLogoUrl);
  73. setIsImageCropModalShow(false);
  74. setIsCustomizedLogoUploaded(true);
  75. toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
  76. }
  77. catch (err) {
  78. toastError(err);
  79. setRetrieveError(err);
  80. throw new Error('Failed to upload brand logo');
  81. }
  82. }, [setUploadLogoSrc, setIsCustomizedLogoUploaded, t, setIsImageCropModalShow, setRetrieveError]);
  83. const closeImageCropModalHandler = useCallback(() => {
  84. if (uploadLogoSrc !== null && typeof uploadLogoSrc !== 'string') {
  85. clearFileInput();
  86. setUploadLogoSrc(null);
  87. }
  88. setIsImageCropModalShow(false);
  89. }, [clearFileInput, setUploadLogoSrc, setIsImageCropModalShow, uploadLogoSrc]);
  90. return (
  91. <React.Fragment>
  92. <div className="row">
  93. <div className="col-12">
  94. <div className="mb-5">
  95. <h2 className="border-bottom my-4 admin-setting-header">{t('admin:customize_settings.custom_logo')}</h2>
  96. <div className="row">
  97. <div className="col-md-6 col-12 mb-3 mb-md-0">
  98. <h4>
  99. <div className="form-check radio-primary">
  100. <input
  101. type="radio"
  102. id="radioDefaultLogo"
  103. className="form-check-input"
  104. form="formImageType"
  105. name="imagetypeForm[isDefaultLogo]"
  106. checked={isDefaultLogoSelected}
  107. onChange={() => { setIsDefaultLogoSelected(true) }}
  108. />
  109. <label className="form-check-label" htmlFor="radioDefaultLogo">
  110. {t('admin:customize_settings.default_logo')}
  111. </label>
  112. </div>
  113. </h4>
  114. <img src={DEFAULT_LOGO} width="64" />
  115. </div>
  116. <div className="col-md-6 col-12">
  117. <h4>
  118. <div className="form-check radio-primary">
  119. <input
  120. type="radio"
  121. id="radioUploadLogo"
  122. className="form-check-input"
  123. form="formImageType"
  124. name="imagetypeForm[isDefaultLogo]"
  125. checked={!isDefaultLogoSelected}
  126. onChange={() => { setIsDefaultLogoSelected(false) }}
  127. />
  128. <label className="form-check-label" htmlFor="radioUploadLogo">
  129. {t('admin:customize_settings.upload_logo')}
  130. </label>
  131. </div>
  132. </h4>
  133. <div className="row mb-3">
  134. <label className="col-sm-4 col-12 col-form-label text-start">
  135. {t('admin:customize_settings.current_logo')}
  136. </label>
  137. <div className="col-sm-8 col-12">
  138. {isCustomizedLogoUploaded && (
  139. <>
  140. <p>
  141. <img src={CUSTOMIZED_LOGO} id="settingBrandLogo" width="64" />
  142. </p>
  143. <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
  144. {t('admin:customize_settings.delete_logo')}
  145. </button>
  146. </>
  147. )}
  148. </div>
  149. </div>
  150. <div className="row">
  151. <label className="col-sm-4 col-12 col-form-label text-start">
  152. {t('admin:customize_settings.upload_new_logo')}
  153. </label>
  154. <div className="col-sm-8 col-12">
  155. <input
  156. type="file"
  157. ref={fileInputRef}
  158. onChange={onSelectFile}
  159. name="brandLogo"
  160. accept="image/*"
  161. />
  162. </div>
  163. </div>
  164. </div>
  165. </div>
  166. <AdminUpdateButtonRow
  167. onClick={onClickSubmit}
  168. disabled={isUpdateButtonDisabled}
  169. />
  170. </div>
  171. </div>
  172. </div>
  173. <ImageCropModal
  174. isShow={isImageCropModalShow}
  175. src={uploadLogoSrc}
  176. onModalClose={closeImageCropModalHandler}
  177. onImageProcessCompleted={processImageCompletedHandler}
  178. isCircular={false}
  179. showCropOption={false}
  180. />
  181. </React.Fragment>
  182. );
  183. };
  184. export default CustomizeLogoSetting;