CustomizeLogoSetting.tsx 8.1 KB

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