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>
</>
);
}
この記事が気に入ったらサポートをしてみませんか?