見出し画像

laravel breeze (react) にavater機能を付ける(6) 仕上げ

今回はソースの整理でかつかなりしんどいので機能だけなら前回のをみとけば大体おkっす。まあ普通にこの文書じゃわからんでしょう。やる気が万が一あるなら行末のソースコードとあわせて解説を見てください。



react-modalでoriginal画像を出してみる

まあこの機能は正直必要ないかもしれんが

resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx 

  const openImageModal = () => setImageModalIsOpen(true);
  const closeImageModal = () => setImageModalIsOpen(false);

オリジナル画像のmodal用関数と

  const [imageModalIsOpen, setImageModalIsOpen] = useState(false); // Added

そのstate

そして

<Modal
  isOpen={imageModalIsOpen}
  onRequestClose={closeImageModal}
  contentLabel="Original Image"
>
  <SecondaryButton className="m-3" onClick={closeImageModal}>Close</SecondaryButton>
  <img src={route('profile.avatar', { user: user.id, t: Date.now() })} />
</Modal>


画像表示Modalを配置しておいて、

        <Avatar
          src={route('profile.avatar', { user: user.id, version: 'thumbnail', t: Date.now() })}
          name={user.name}
          size="100"
          round
          maxInitials={1}
          onClick={avatarExists ? () => openImageModal() : undefined}
          className={avatarExists ? "cursor-pointer" : ""}
        />

Avatarのonclick属性にmodalのopenを加える

コンポーネント化

とりあえず同一ファイルに切り出していくのが初手ではないかね?

Avatarの部分とそのopenのmodal


<Avatar
  src={route('profile.avatar', { user: user.id, version: 'thumbnail', t: Date.now() })}
  name={user.name}
  size="100"
  round
  maxInitials={1}
  onClick={avatarExists ? () => openImageModal() : undefined}
  className={avatarExists ? "cursor-pointer" : ""}
/>

前準備

これをShowAvatarWithModalみたいなcomponentに移動する。関数はexport外に書くぞ

  const ShowAvatarWithModal = () => {
  }
        <ShowAvatarWithModal />

<Avatar
  src={route('profile.avatar', { user: user.id, version: 'thumbnail', t: Date.now() })}
  name={user.name}
  size="100"
  round
  maxInitials={1}
  onClick={avatarExists ? () => openImageModal() : undefined}
  className={avatarExists ? "cursor-pointer" : ""}
/>

このように空を配置しておく。では移動してみよう。

const ShowAvatarWithModal = () => {
  return (
    <Avatar
      src={route('profile.avatar', { user: user.id, version: 'thumbnail', t: Date.now() })}
      name={user.name}
      size="100"
      round
      maxInitials={1}
      onClick={avatarExists ? () => openImageModal() : undefined}
      className={avatarExists ? "cursor-pointer" : ""}
    />
  );
}

するとまず

UpdateProfileInformationForm.jsx:36 Uncaught ReferenceError: user is not defined

というエラーが出る。当然userがない。userは親から渡してもいいけど

  const { user } = usePage().props.auth;

これで取れる

avatarExists is not defined

これも

  const avatarExists = usePage().props.avatarExists;

このようにusePageで取れる。これで一通り動きはじめるが画像をクリックすると


openImageModal is not defined

などいわれるので、stateも移動する

const ShowAvatarWithModal = () => {
  const { user } = usePage().props.auth;
  const avatarExists = usePage().props.avatarExists;
  const [imageModalIsOpen, setImageModalIsOpen] = useState(false); // Added
  const openImageModal = () => setImageModalIsOpen(true);
  const closeImageModal = () => setImageModalIsOpen(false);

  return (
    <Avatar
      src={route('profile.avatar', { user: user.id, version: 'thumbnail', t: Date.now() })}
      name={user.name}
      size="100"
      round
      maxInitials={1}
      onClick={avatarExists ? () => openImageModal() : undefined}
      className={avatarExists ? "cursor-pointer" : ""}
    />
  );
}

こんな感じね

imageModalIsOpen is not defined

これはとりあえず親コンポーネントのmodalを一旦停止る

      {/*
      <Modal
        isOpen={imageModalIsOpen}
        onRequestClose={closeImageModal}
        contentLabel="Original Image"
      >
        <SecondaryButton className="m-3" onClick={closeImageModal}>Close</SecondaryButton>
        <img src={route('profile.avatar', { user: user.id, t: Date.now() })} />
      </Modal>
      */}

そしたら

const ShowAvatarWithModal = () => {
  const { user } = usePage().props.auth;
  const avatarExists = usePage().props.avatarExists;
  const [imageModalIsOpen, setImageModalIsOpen] = useState(false); // Added
  const openImageModal = () => setImageModalIsOpen(true);
  const closeImageModal = () => setImageModalIsOpen(false);

  return (
    <>
      <Avatar
        src={route('profile.avatar', { user: user.id, version: 'thumbnail', t: Date.now() })}
        name={user.name}
        size="100"
        round
        maxInitials={1}
        onClick={avatarExists ? () => openImageModal() : undefined}
        className={avatarExists ? "cursor-pointer" : ""}
      />
      <Modal
        isOpen={imageModalIsOpen}
        onRequestClose={closeImageModal}
        contentLabel="Original Image"
      >
        <SecondaryButton className="m-3" onClick={closeImageModal}>Close</SecondaryButton>
        <img src={route('profile.avatar', { user: user.id, t: Date.now() })} />
      </Modal>
    </>
  );
}

ここに移動してくる。並列なので <> </>を配置した

これで表示系は全部移動できたはずだ。

filecropの辺を移動する


const FilePondAndCrop = () => {
}

やはり最初に空コンポーネントを定義する、てかもう最初からreturnしとこ

const FilePondAndCrop = () => {
  return (
    <>
    </>
  );
}

そしたら

        <div>
          <InputLabel htmlFor="avatar" value="Avatar" />
          <FilePondAndCrop />
          <FilePond
            ref={filePondRef}
            name="avatar"
            onupdatefiles={(files) => {
              if (files.length > 0) {
                setData('avatar', files[0].file);
              } else {
                setData('avatar', null);
              }
            }}
            allowMultiple={false}
            acceptedFileTypes={['image/jpeg', 'image/png', 'image/gif']}
            maxFileSize="5MB"
            imageEditEditor={editor}
            {...localeJA}
          />
          <InputError className="mt-2" message={errors.avatar} />
        </div>

この辺でしかけていってみよう

const FilePondAndCrop = ({onFileUpdate}) => {
  return (
    <FilePond
      onupdatefiles={onFileUpdate}
    />
  );
}

そしたら親

  const handleFileUpdate = (files) => {
    if (files.length > 0) {
      setData('avatar', files[0].file);
    } else {
      setData('avatar', null);
    }
  };

データーのセッターを一応作っといてーの

        <div>
          <InputLabel htmlFor="avatar" value="Avatar" />
          <FilePondAndCrop onFileUpdate={handleFileUpdate} />

これで関数が渡ってくる。そうしたら

const FilePondAndCrop = ({onFileUpdate}) => {
  return (
    <FilePond
      onupdatefiles={onFileUpdate}
      name="avatar"
      allowMultiple={false}
      acceptedFileTypes={['image/jpeg', 'image/png', 'image/gif']}
      maxFileSize="5MB"
      imageEditEditor={editor}
      {...localeJA}
    />
  );
}

ずばっと移植し、editorを移動する

const FilePondAndCrop = ({onFileUpdate}) => {
  const editor = {
    open: (file, imageEditCallback) => {
    }
  };

  return (
    <FilePond
      onupdatefiles={onFileUpdate}
      name="avatar"
      allowMultiple={false}
      acceptedFileTypes={['image/jpeg', 'image/png', 'image/gif']}
      maxFileSize="5MB"
      imageEditEditor={editor}
      {...localeJA}
    />
  );
}


Editボタンが付与されたのを確認したらopenを移動していく

const FilePondAndCrop = ({onFileUpdate}) => {
  const editor = {
    open: (file, imageEditCallback) => {
      const objectURL = URL.createObjectURL(file);
      setCropperUrl(objectURL); // objectURLを状態にセット


      /*
      setOriginalFile(file);
      const imageElement = cropperRef.current;

      if (imageElement && imageElement.cropper) {
        imageElement.cropper.replace(objectURL);
      }
      setCropModalIsOpen(true); // Added
      */
    }
  };

ここが最低限cropperのrefに依存しているので移動しないといけない

      {/*
      <Modal
        isOpen={cropModalIsOpen}
        onRequestClose={closeCropModal}
        contentLabel="Cropping"
      >
        <Cropper
          ref={cropperRef}
          src={cropperUrl}
          aspectRatio={1}
          className="h-1/3 sm:h-1/2 md:h-2/3 lg:h-3/4" // 画面の高さに対する割合で高さを設定
          viewMode={1}
        />

        <div className="my-2">
          <PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
          <SecondaryButton className="ml-2" onClick={closeCropModal}>Cancel</SecondaryButton>
        </div>
      </Modal>
      */}

Modalを一旦やめて

  return (
    <>
    <FilePond
      onupdatefiles={onFileUpdate}
      name="avatar"
      allowMultiple={false}
      acceptedFileTypes={['image/jpeg', 'image/png', 'image/gif']}
      maxFileSize="5MB"
      imageEditEditor={editor}
      {...localeJA}
    />
    <Cropper
    // ref={cropperRef}
    // src={cropperUrl}
    // className="h-1/3 sm:h-1/2 md:h-2/3 lg:h-3/4" // 画面の高さに対する割合で高さを設定
      aspectRatio={1}
      viewMode={1}
    />
    </>
  );

またcropperを持ってくる。そしたらrefをセットする。ここではcropperだけでまずいいだろう

const FilePondAndCrop = ({onFileUpdate}) => {
  const cropperRef = useRef(null);

そしたらばどんどん移植していく

const FilePondAndCrop = ({onFileUpdate}) => {
  const cropperRef = useRef(null);
  const [cropperUrl, setCropperUrl] = useState(null);
  const editor = {
    open: (file, imageEditCallback) => {
      const objectURL = URL.createObjectURL(file);
      setCropperUrl(objectURL); // objectURLを状態にセット
      const imageElement = cropperRef.current;
      if (imageElement && imageElement.cropper) {
        imageElement.cropper.replace(objectURL);
      }
      // setOriginalFile(file);
      // setCropModalIsOpen(true); // Added
    }
  };

  return (
    <>
    <FilePond
      onupdatefiles={onFileUpdate}
      name="avatar"
      allowMultiple={false}
      acceptedFileTypes={['image/jpeg', 'image/png', 'image/gif']}
      maxFileSize="5MB"
      imageEditEditor={editor}
      {...localeJA}
    />
    <Cropper
      ref={cropperRef}
      src={cropperUrl}
    // className="h-1/3 sm:h-1/2 md:h-2/3 lg:h-3/4" // 画面の高さに対する割合で高さを設定
      aspectRatio={1}
      viewMode={1}
    />
    </>
  );
}

originalFileのセッターも移動する

const FilePondAndCrop = ({onFileUpdate}) => {
  const cropperRef = useRef(null);
  const [cropperUrl, setCropperUrl] = useState(null);
  const [originalFile, setOriginalFile] = useState(null);
  const editor = {
    open: (file, imageEditCallback) => {
      const objectURL = URL.createObjectURL(file);
      setCropperUrl(objectURL); // objectURLを状態にセット
      const imageElement = cropperRef.current;
      if (imageElement && imageElement.cropper) {
        imageElement.cropper.replace(objectURL);
      }
      setOriginalFile(file);
      // setCropModalIsOpen(true); // Added
    }
  };

  return (
    <>
    <FilePond
      onupdatefiles={onFileUpdate}
      name="avatar"
      allowMultiple={false}
      acceptedFileTypes={['image/jpeg', 'image/png', 'image/gif']}
      maxFileSize="5MB"
      imageEditEditor={editor}
      {...localeJA}
    />
    <Cropper
      ref={cropperRef}
      src={cropperUrl}
    // className="h-1/3 sm:h-1/2 md:h-2/3 lg:h-3/4" // 画面の高さに対する割合で高さを設定
      aspectRatio={1}
      viewMode={1}
    />
    </>
  );
}

そしたら

    <div className="my-2">
      <PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
      {/*
      <SecondaryButton className="ml-2" onClick={closeCropModal}>Cancel</SecondaryButton>
      */}
    </div>

これを置いてcrop関数をも移動する

const FilePondAndCrop = ({onFileUpdate}) => {
  const cropperRef = useRef(null);
  const [cropperUrl, setCropperUrl] = useState(null);
  const [originalFile, setOriginalFile] = useState(null);
  const editor = {
    open: (file, imageEditCallback) => {
      const objectURL = URL.createObjectURL(file);
      setCropperUrl(objectURL); // objectURLを状態にセット
      const imageElement = cropperRef.current;
      if (imageElement && imageElement.cropper) {
        imageElement.cropper.replace(objectURL);
      }
      setOriginalFile(file);
      // setCropModalIsOpen(true); // Added
    }
  };
  const crop = () => {
    const imageElement = cropperRef.current;
    if (imageElement && imageElement.cropper) {
      const canvas = imageElement.cropper.getCroppedCanvas();
      canvas.toBlob((blob) => {
        // FilePondの`addFile`メソッドを使用して、編集後のBlobをFilePondインスタンスに追加
        filePondRef.current.addFile(blob);

        // オブジェクトURLをクリア
        if (cropperUrl) {
          URL.revokeObjectURL(cropperUrl); // メモリを開放
          setCropperUrl(null); // cropperUrlの状態をクリア
        }
      }, originalFile.type);
    }
    // setCropModalIsOpen(false);
  };

そうすと、ここで

 filePondRef.current.addFile(blob);

的にfilepondのrefを参照しているから、さらに移植する

const FilePondAndCrop = ({onFileUpdate}) => {
  const cropperRef = useRef(null);
  const filePondRef = useRef(null);
  const [cropperUrl, setCropperUrl] = useState(null);
  const [originalFile, setOriginalFile] = useState(null);
  const editor = {
    open: (file, imageEditCallback) => {
      const objectURL = URL.createObjectURL(file);
      setCropperUrl(objectURL); // objectURLを状態にセット
      const imageElement = cropperRef.current;
      if (imageElement && imageElement.cropper) {
        imageElement.cropper.replace(objectURL);
      }
      setOriginalFile(file);
      // setCropModalIsOpen(true); // Added
    }
  };
  const crop = () => {
    const imageElement = cropperRef.current;
    if (imageElement && imageElement.cropper) {
      const canvas = imageElement.cropper.getCroppedCanvas();
      canvas.toBlob((blob) => {
        // FilePondの`addFile`メソッドを使用して、編集後のBlobをFilePondインスタンスに追加
        filePondRef.current.addFile(blob);

        // オブジェクトURLをクリア
        if (cropperUrl) {
          URL.revokeObjectURL(cropperUrl); // メモリを開放
          setCropperUrl(null); // cropperUrlの状態をクリア
        }
      }, originalFile.type);
    }
    // setCropModalIsOpen(false);
  };

  return (
    <>
    <FilePond
      ref={filePondRef}
      onupdatefiles={onFileUpdate}
      name="avatar"
      allowMultiple={false}
      acceptedFileTypes={['image/jpeg', 'image/png', 'image/gif']}
      maxFileSize="5MB"
      imageEditEditor={editor}
      {...localeJA}
    />
    <Cropper
      ref={cropperRef}
      src={cropperUrl}
    // className="h-1/3 sm:h-1/2 md:h-2/3 lg:h-3/4" // 画面の高さに対する割合で高さを設定
      aspectRatio={1}
      viewMode={1}
    />
    <div className="my-2">
      <PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
      {/*
      <SecondaryButton className="ml-2" onClick={closeCropModal}>Cancel</SecondaryButton>
      */}
    </div>
    </>
  );
}

さて、ここまででcropと連携できているはずだ

modalの処理

modalもこのコンポーネントの中にぶちこんでよさそう。

modalの移植


まずコンポーネントにmodalを書いてみよう

    <Modal
      isOpen={cropModalIsOpen}
    // onRequestClose={closeCropModal}
      contentLabel="Cropping"
    >
    </Modal>

ここで

const FilePondAndCrop = ({onFileUpdate}) => {
  const cropperRef = useRef(null);
  const filePondRef = useRef(null);
  const [cropperUrl, setCropperUrl] = useState(null);
  const [originalFile, setOriginalFile] = useState(null);
  const [cropModalIsOpen, setCropModalIsOpen] = useState(false);

そしたら

const FilePondAndCrop = ({onFileUpdate}) => {
  const cropperRef = useRef(null);
  const filePondRef = useRef(null);
  const [cropperUrl, setCropperUrl] = useState(null);
  const [originalFile, setOriginalFile] = useState(null);
  const [cropModalIsOpen, setCropModalIsOpen] = useState(false);

  const editor = {
    open: (file, imageEditCallback) => {
      const objectURL = URL.createObjectURL(file);
      setCropperUrl(objectURL); // objectURLを状態にセット
      const imageElement = cropperRef.current;
      if (imageElement && imageElement.cropper) {
        imageElement.cropper.replace(objectURL);
      }
      setOriginalFile(file);
      setCropModalIsOpen(true);
    }
  };

してやるとeditを押すとmodalが起動してくる。ここでcloseを移動する

  const closeCropModal = () => {
    setCropModalIsOpen(false);
    if (cropperUrl) {
      URL.revokeObjectURL(cropperUrl);
      setCropperUrl(null);
    }
    setOriginalFile(null);
  };

そしたら

    <div className="my-2">
      <PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
      <SecondaryButton className="ml-2" onClick={closeCropModal}>Cancel</SecondaryButton>
    </div>

これを復活させて

    <Modal
      isOpen={cropModalIsOpen}
      onRequestClose={closeCropModal}
      contentLabel="Cropping"
    >
      <div className="my-2">
        <PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
        <SecondaryButton className="ml-2" onClick={closeCropModal}>Cancel</SecondaryButton>
      </div>
    </Modal>

modalの中にぶちこむ。このときCancelがちゃんと効くことを確認したらCropperを移動する

    <>
      <FilePond
        ref={filePondRef}
        onupdatefiles={onFileUpdate}
        name="avatar"
        allowMultiple={false}
        acceptedFileTypes={['image/jpeg', 'image/png', 'image/gif']}
        maxFileSize="5MB"
        imageEditEditor={editor}
        {...localeJA}
      />
      <Modal
        isOpen={cropModalIsOpen}
        onRequestClose={closeCropModal}
        contentLabel="Cropping"
      >
        <Cropper
          ref={cropperRef}
          src={cropperUrl}
          className="h-1/3 sm:h-1/2 md:h-2/3 lg:h-3/4" // 画面の高さに対する割合で高さを設定
          aspectRatio={1}
          viewMode={1}
        />
        <div className="my-2">
          <PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
          <SecondaryButton className="ml-2" onClick={closeCropModal}>Cancel</SecondaryButton>
        </div>
      </Modal>
    </>

clearFile

あとはこれだけど、なんか労力の割に効果うすいからやる気でねえなあ。。。とりあえずなしでいいかw

  /* // TODO
  const clearFile = () => {
    if (filePondRef.current) {
      filePondRef.current.removeFile();
    }
    reset('avatar');
  };
  */

でましたTODO

  const submit = (e) => {
    e.preventDefault();
    post(route('profile.update')) // TODO , { onSuccess: () => clearFile() });
  };

ここも

fileに分ける

なーんかcomponentって感じでもないし、Partialsに分けときますか…

import React, { useRef, useState, useEffect } from 'react';
import {
  Link, useForm, usePage, router,
} from '@inertiajs/react';
import { Transition } from '@headlessui/react';
import FilePondAndCrop from '../Partials/FilePondAndCrop';

としといて resources/js/Pages/Profile/Partials/FilePondAndCrop.jsx 

export default function FilePondAndCrop({ onFileUpdate }) {
  const cropperRef = useRef(null);
  const filePondRef = useRef(null);
  const [cropperUrl, setCropperUrl] = useState(null);
  const [originalFile, setOriginalFile] = useState(null);
  const [cropModalIsOpen, setCropModalIsOpen] = useState(false);

  const editor = {
    open: (file, imageEditCallback) => {
      const objectURL = URL.createObjectURL(file);
      setCropperUrl(objectURL); // objectURLを状態にセット
      const imageElement = cropperRef.current;
      if (imageElement && imageElement.cropper) {
        imageElement.cropper.replace(objectURL);
      }
      setOriginalFile(file);
      setCropModalIsOpen(true);
    },
  };
  const crop = () => {
    const imageElement = cropperRef.current;
    if (imageElement && imageElement.cropper) {
      const canvas = imageElement.cropper.getCroppedCanvas();
      canvas.toBlob((blob) => {
        // FilePondの`addFile`メソッドを使用して、編集後のBlobをFilePondインスタンスに追加
        filePondRef.current.addFile(blob);

        // オブジェクトURLをクリア
        if (cropperUrl) {
          URL.revokeObjectURL(cropperUrl); // メモリを開放
          setCropperUrl(null); // cropperUrlの状態をクリア
        }
      }, originalFile.type);
    }
    // setCropModalIsOpen(false);
  };
  const closeCropModal = () => {
    setCropModalIsOpen(false);
    if (cropperUrl) {
      URL.revokeObjectURL(cropperUrl);
      setCropperUrl(null);
    }
    setOriginalFile(null);
  };
  /* // TODO
  const clearFile = () => {
    if (filePondRef.current) {
      filePondRef.current.removeFile();
    }
    reset('avatar');
  };
  */

  return (
    <>
      <FilePond
        ref={filePondRef}
        onupdatefiles={onFileUpdate}
        name="avatar"
        allowMultiple={false}
        acceptedFileTypes={['image/jpeg', 'image/png', 'image/gif']}
        maxFileSize="5MB"
        imageEditEditor={editor}
        {...localeJA}
      />
      <Modal
        isOpen={cropModalIsOpen}
        onRequestClose={closeCropModal}
        contentLabel="Cropping"
      >
        <Cropper
          ref={cropperRef}
          src={cropperUrl}
          className="h-1/3 sm:h-1/2 md:h-2/3 lg:h-3/4" // 画面の高さに対する割合で高さを設定
          aspectRatio={1}
          viewMode={1}
        />
        <div className="my-2">
          <PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
          <SecondaryButton className="ml-2" onClick={closeCropModal}>Cancel</SecondaryButton>
        </div>
      </Modal>
    </>
  );
}

ガバっと移植する。当然エラーの連続なので、どんどん移動する

さらに ShowAvatarWithModal も移動するぞ

最終ソース

resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx 

import React, { useEffect } from 'react';
import { Link, useForm, usePage } from '@inertiajs/react';
import { Transition } from '@headlessui/react';
import Modal from 'react-modal';
import PrimaryButton from '@/Components/PrimaryButton';
import SecondaryButton from '@/Components/SecondaryButton';
import TextInput from '@/Components/TextInput';
import InputLabel from '@/Components/InputLabel';
import InputError from '@/Components/InputError';

// External Components
import FilePondAndCrop from '../Partials/FilePondAndCrop';
import ShowAvatarWithModal from '../Partials/ShowAvatarWithModal';

export default function UpdateProfileInformation({ mustVerifyEmail, status, className = '' }) {
  const { user } = usePage().props.auth;
  useEffect(() => {
    Modal.setAppElement('#app');
  }, []);

  const {
    data, setData, post, errors, processing, recentlySuccessful, reset,
  } = useForm({
    name: user.name,
    email: user.email,
    avatar: null,
    _method: 'patch',
  });

  const submit = (e) => {
    e.preventDefault();
    post(route('profile.update')); // TODO , { onSuccess: () => clearFile() });
  };

  const handleFileUpdate = (files) => {
    if (files.length > 0) {
      setData('avatar', files[0].file);
    } else {
      setData('avatar', null);
    }
  };

  return (
    <section className={className}>
      <header>
        <h2 className="text-lg font-medium text-gray-900">Profile Information</h2>

        <p className="mt-1 text-sm text-gray-600">
          Update your account's profile information and email address.
        </p>
        <ShowAvatarWithModal />
      </header>

      <form onSubmit={submit} className="mt-6 space-y-6">
        <div>
          <InputLabel htmlFor="name" value="Name" />

          <TextInput
            id="name"
            className="mt-1 block w-full"
            value={data.name}
            onChange={(e) => setData('name', e.target.value)}
            required
            isFocused
            autoComplete="name"
          />

          <InputError className="mt-2" message={errors.name} />
        </div>

        <div>
          <InputLabel htmlFor="email" value="Email" />

          <TextInput
            id="email"
            type="email"
            className="mt-1 block w-full"
            value={data.email}
            onChange={(e) => setData('email', e.target.value)}
            required
            autoComplete="username"
          />

          <InputError className="mt-2" message={errors.email} />
        </div>

        {mustVerifyEmail && user.email_verified_at === null && (
        <div>
          <p className="text-sm mt-2 text-gray-800">
            Your email address is unverified.
            <Link
              href={route('verification.send')}
              method="post"
              as="button"
              className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
            >
              Click here to re-send the verification email.
            </Link>
          </p>

          {status === 'verification-link-sent' && (
          <div className="mt-2 font-medium text-sm text-green-600">
            A new verification link has been sent to your email address.
          </div>
          )}
        </div>
        )}

        <div>
          <InputLabel htmlFor="avatar" value="Avatar" />
          <FilePondAndCrop onFileUpdate={handleFileUpdate} />
          <InputError className="mt-2" message={errors.avatar} />
        </div>

        <div className="flex items-center gap-4">
          <PrimaryButton disabled={processing}>Save</PrimaryButton>
          <Transition
            show={recentlySuccessful}
            enter="transition ease-in-out"
            enterFrom="opacity-0"
            leave="transition ease-in-out"
            leaveTo="opacity-0"
          >
            <p className="text-sm text-gray-600">Saved.</p>
          </Transition>
        </div>
      </form>
    </section>
  );
}

resources/js/Pages/Profile/Partials/FilePondAndCrop.jsx 
filepondとcroppoing

import React, { useRef, useState, useEffect } from 'react';
import PrimaryButton from '@/Components/PrimaryButton';

import { FilePond, registerPlugin } from 'react-filepond';
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size';
import FilePondPluginImageEdit from 'filepond-plugin-image-edit';
import localeJA from 'filepond/locale/ja-ja.js';
import 'filepond/dist/filepond.min.css';
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css';
import Modal from 'react-modal';

import Cropper from 'react-cropper';
import SecondaryButton from '@/Components/SecondaryButton';
import 'cropperjs/dist/cropper.css';

registerPlugin(
  FilePondPluginFileValidateType,
  FilePondPluginImagePreview,
  FilePondPluginFileValidateSize,
  FilePondPluginImageEdit,
);

export default function FilePondAndCrop({ onFileUpdate }) {
  const cropperRef = useRef(null);
  const filePondRef = useRef(null);
  const [cropperUrl, setCropperUrl] = useState(null);
  const [originalFile, setOriginalFile] = useState(null);
  const [cropModalIsOpen, setCropModalIsOpen] = useState(false);

  const editor = {
    open: (file, imageEditCallback) => {
      const objectURL = URL.createObjectURL(file);
      setCropperUrl(objectURL); // objectURLを状態にセット
      const imageElement = cropperRef.current;
      if (imageElement && imageElement.cropper) {
        imageElement.cropper.replace(objectURL);
      }
      setOriginalFile(file);
      setCropModalIsOpen(true);
    },
  };
  const crop = () => {
    const imageElement = cropperRef.current;
    if (imageElement && imageElement.cropper) {
      const canvas = imageElement.cropper.getCroppedCanvas();
      canvas.toBlob((blob) => {
        // FilePondの`addFile`メソッドを使用して、編集後のBlobをFilePondインスタンスに追加
        filePondRef.current.addFile(blob);

        // オブジェクトURLをクリア
        if (cropperUrl) {
          URL.revokeObjectURL(cropperUrl); // メモリを開放
          setCropperUrl(null); // cropperUrlの状態をクリア
        }
      }, originalFile.type);
    }
    setCropModalIsOpen(false);
  };
  const closeCropModal = () => {
    setCropModalIsOpen(false);
    if (cropperUrl) {
      URL.revokeObjectURL(cropperUrl);
      setCropperUrl(null);
    }
    setOriginalFile(null);
  };
  /* // TODO
  const clearFile = () => {
    if (filePondRef.current) {
      filePondRef.current.removeFile();
    }
    reset('avatar');
  };
  */

  return (
    <>
      <FilePond
        ref={filePondRef}
        onupdatefiles={onFileUpdate}
        name="avatar"
        allowMultiple={false}
        acceptedFileTypes={['image/jpeg', 'image/png', 'image/gif']}
        maxFileSize="5MB"
        imageEditEditor={editor}
        {...localeJA}
      />
      <Modal
        isOpen={cropModalIsOpen}
        onRequestClose={closeCropModal}
        contentLabel="Cropping"
      >
        <Cropper
          ref={cropperRef}
          src={cropperUrl}
          className="h-1/3 sm:h-1/2 md:h-2/3 lg:h-3/4" // 画面の高さに対する割合で高さを設定
          aspectRatio={1}
          viewMode={1}
        />
        <div className="my-2">
          <PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
          <SecondaryButton className="ml-2" onClick={closeCropModal}>Cancel</SecondaryButton>
        </div>
      </Modal>
    </>
  );
}

resources/js/Pages/Profile/Partials/ShowAvatarWithModal.jsx avatar表示

import React, { useState } from 'react';
import { usePage } from '@inertiajs/react';

import Avatar from 'react-avatar';
import Modal from 'react-modal';
import SecondaryButton from '@/Components/SecondaryButton';

export default function ShowAvatarWithModal() {
  const { user } = usePage().props.auth;
  const { avatarExists } = usePage().props;
  const [imageModalIsOpen, setImageModalIsOpen] = useState(false); // Added
  const openImageModal = () => setImageModalIsOpen(true);
  const closeImageModal = () => setImageModalIsOpen(false);

  return (
    <>
      <Avatar
        src={route('profile.avatar', { user: user.id, version: 'thumbnail', t: Date.now() })}
        name={user.name}
        size="100"
        round
        maxInitials={1}
        onClick={avatarExists ? () => openImageModal() : undefined}
        className={avatarExists ? 'cursor-pointer' : ''}
      />
      <Modal
        isOpen={imageModalIsOpen}
        onRequestClose={closeImageModal}
        contentLabel="Original Image"
      >
        <SecondaryButton className="m-3" onClick={closeImageModal}>Close</SecondaryButton>
        <img src={route('profile.avatar', { user: user.id, t: Date.now() })} />
      </Modal>
    </>
  );
}





この記事が気に入ったらサポートをしてみませんか?