見出し画像

laravel breeze (react) にavater機能を付ける(3) - 画像の表示 (react-avatar)

前回

画像の表示なんて適当にimgしておけばいいんじゃないの?っていうのもあるけど、実際にはちょっと勝手は違う。とりあえず今メディアをpublicに剥き出しにしていないので画像を吐き出すためのrouteが必要であるので、そこから作っていく(いやまあ要件によっては剥き出しにしてもいいけど…)

routes/web.php

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');

    // アバター画像へのルート
    Route::get('/profile/avatar/{version?}', [ProfileController::class, 'showAvatar'])
        ->name('profile.avatar');
});

このようなrouteを付け加えた。そうするとshowAvatarというメソッドが必要となってくる。真面目にタイプヒンティングも付けておる。

use \Symfony\Component\HttpFoundation\BinaryFileResponse ;
class ProfileController extends Controller
{
// ...
    public function showAvatar(Request $request, $version = null): BinaryFileResponse
    {
        $user = $request->user();
        $media = $user->getFirstMedia('avatars'); // 最初のavatarイメージといっても今回はアップロードされるのは1ファイルではあるが

        if (!$media) {
            abort(404, 'Avatar not found');
        }

        $path = $version ? $media->getPath($version) : $media->getPath();

        if (!file_exists($path)) {
            abort(404, 'Avatar version not found');
        }

        return response()->file($path);
    }

でまあここまでくると、imgタグで放出できるようにはなる。

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

      <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>

        <img src={route("profile.avatar", {version: "thumbnail"})} />
      </header>


ただし、キャッシュが強烈に効くのとSPAだけあってブラウザがフルリフレッシュされないので、やってみるとわかるんだけど新たな画像をアップロードしても即座に反映されないかもしれない

クエリーを付ける事での対応

この件で一番単純なのは

<img src={route("profile.avatar", { version: "thumbnail", t: Date.now() })} />

みたいにt={timestamp}とか付けてキャッシュを強制的に無効にしたりとかだろう。

routeの調整

ただ、avatarというものの特性を考えてみた時に、同一システムの他人の画像を見たいというのはある。これに関しては単純に

    // アバター画像へのルート
    Route::get('/avatar/{user}/{version?}', [ProfileController::class, 'showAvatar'])
        ->name('profile.avatar');

のようにuserのIDをURLに埋めこんでしまうのが一番早い。早いんだけども、数字を探られたりしてやだなーって人もいるだろう、そういう場合は頑張って考えてみてくださいという事で、一応の問題提起だけして後はぶん投げときます。

でまあ最終的にuidを取る場合は

<img src={route("profile.avatar", { user: user.id, version: "thumbnail", t: Date.now() })} />

としておく。backend(laravel)はshowAvatar関数を見るので

use App\Models\User;

class ProfileController extends Controller
{
// <snip>

    public function showAvatar(Request $request, User $user, $version = null): BinaryFileResponse
    {
        $media = $user->getFirstMedia('avatars');

        if (!$media) {
            abort(404, 'Avatar not found');
        }

        $path = $version ? $media->getPath($version) : $media->getPath();

        if (!file_exists($path)) {
            abort(404, 'Avatar version not found');
        }

        return response()->file($path);
    }
}

などする。versionによってthumbnailだったりiconだったりを出すことにするぞい。

アイコンが無い時の対応

もちろんnoimage.pngみたいなのを用意してもいいけど、ここではreact-avatar を使ってみよう。npm installしといてくださいな

npm install react-avatar

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

import Avatar from 'react-avatar';

で追加。

<img src={route("profile.avatar", { user: user.id, version: "thumbnail", t: Date.now() })} />
<Avatar
  name={user.name}
  size="100"
  round={true}
  maxInitials={1}
/>

このように並べると

などとなる。画像がある時はsrcに渡せばよい。

<Avatar
  src={route("profile.avatar", { user: user.id, version: "thumbnail", t: Date.now() })}
  name={user.name}
  size="100"
  round={true}
  maxInitials={1}
/>

まあ自動的にroundになるのでそれっぽい。で、たとえばDBを初期化し、ファイルを削除すると

このようにはなってくれる。とりあえず今回はこれ以上は踏みこまない。次回作では踏みこむかもしれない。

navbarの件

resources/js/Layouts/AuthenticatedLayout.jsx ここのところのアイコン

{user.name}

で出しているものを

<Avatar
  src={route("profile.avatar", { user: user.id, version: "thumbnail", t: Date.now() })}
  name={user.name}
  size="40"
  round={true}
  maxInitials={1}
/>

などすると


こうなる。

<Avatar
  src={route("profile.avatar", { user: user.id, version: "thumbnail", t: Date.now() })}
  name={user.name}
  size="40"
  round={true}
  maxInitials={1}
  className="hover:bg-gray-200 hover:shadow"
/>

などしてあげればtailwind cssでちょっと、本当にわずかにエフェクトが付く


これじゃわからんて…

が、さらにエフェクトをマシたい場合はもうスタイルを追加するしかないな。

      <style>
{`
.avatar-hover {
  transition: box-shadow 0.3s ease-in-out;
}

.avatar-hover:hover {
  box-shadow: 0 0 15px rgba(0,0,0,0.3);
}
        `}
      </style>
<Avatar
  src={route("profile.avatar", { user: user.id, version: "thumbnail", t: Date.now() })}
  name={user.name}
  size="40"
  round={true}
  maxInitials={1}  
  className="avatar-hover"
  // className="hover:bg-gray-200 hover:shadow"
/>

ちなみに、アバターを適用したらこういう状態になるはず。あ、ちなみに、


さて、まだ終わりではない。というかまだ道半ば感が凄いある。次回はfilepondを使ってもう少しリッチなUIにしたりする、かな?

そうそう

面倒だけどレスポンシブのビューにも付けといた方がいいすよ。

resources/js/Layouts/AuthenticatedLayout.jsx 

        <div className={`${showingNavigationDropdown ? 'block' : 'hidden'} sm:hidden`}>
          <div className="pt-2 pb-3 space-y-1">
            <ResponsiveNavLink href={route('dashboard')} active={route().current('dashboard')}>
              Dashboard
            </ResponsiveNavLink>
          </div>

          <div className="pt-4 pb-1 border-t border-gray-200">
            <div className="px-4">
              <div className="font-medium text-base text-gray-800">
                <Avatar
                  src={route("profile.avatar", { user: user.id, version: "thumbnail", t: Date.now() })}
                  name={user.name}
                  size="40"
                  round={true}
                  maxInitials={1}
                  className="mr-2 mb-2"
                />
                {user.name}
              </div>
              <div className="font-medium text-sm text-gray-500">{user.email}</div>
            </div>


こんなもんかな?ただ、このメニュー全般的に見辛いっすよねw

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