前回は、Firestore Database を使い、React でメッセージ送信機能を実装しました。

今回は、プロフィール編集画面を作成し、Firestore Storage にアバター画像を保存します。

まずは、ヘッダーのプロフィールをクリックすると、プロフィール画面へ遷移するようにします。

MUI でプロフィール画面を作りましょう。

import React, { useState } from "react"
import {
  Paper,
  Typography,
  Box,
  TextField,
  Button,
  Container,
} from "@mui/material"

const Profile = () => {
  const [name, setName] = useState("")

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.files)
  }

  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault()
  }

  return (
    <Container maxWidth="sm">
      <Paper sx={{ m: 4, p: 4 }}>
        <Typography align="center">プロフィール編集</Typography>
        <Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 4 }}>
          <input type="file" accept="image/*" onChange={handleChange} />

          <TextField
            margin="normal"
            required
            fullWidth
            id="name"
            label="ユーザー名"
            name="name"
            autoComplete="name"
            autoFocus
            value={name}
            onChange={e => setName(e.target.value)}
          />
          <Button
            type="submit"
            fullWidth
            variant="contained"
            sx={{ mt: 3, mb: 2 }}
          >
            保存
          </Button>
        </Box>
      </Paper>
    </Container>
  )
}

export default Profile

image2

次に、react-router-domでプロフィール画面のパスを作成します。

App.tsx へ移動し、Routeを追加します。

<Route path="profile" element={<Profile />} />

Header.tsx へ移動し、リンクを設定しましょう。

<MenuItem onClick={handleClose}>
  <Link href="profile" underline="none" color="inherit">
    プロフィール
  </Link>
</MenuItem>

プロフィール画面もヘッダーを表示させたいので、App.tsx でホーム画面とプロフィール画面のみヘッダーを表示するようにします。

react-router-domからOutletをインポートします。

import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom"

Outletchildrenみたいな役割を果たしてくれます。

Layout関数を作成します。

Layout関数の中にHeaderOutletを設定します。

function Layout() {
  return (
    <>
      <Header />
      <Outlet />
    </>
  )
}

App関数のRoutesの中にLayoutを設定します。

function App() {
  return (
    <ThemeProvider theme={theme}>
      <BrowserRouter>
        <Routes>
          <Route element={<Layout />}>
            <Route path="/" element={<Home />} />
            <Route path="profile" element={<Profile />} />
          </Route>
          <Route path="login" element={<Login />} />
          <Route path="signup" element={<Signup />} />
          <Route path="password-reset" element={<PasswordReset />} />
        </Routes>
      </BrowserRouter>
    </ThemeProvider>
  )
}

プロフィール画面を確認すると、

image3

ヘッダーが表示されました。

ちなみに、ログイン画面では、

image4

ヘッダーが表示されていません。

このままでは、ホーム画面でヘッダーが二重で表示されるので、Home.tsx のHeaderは削除しておきましょう。

『ファイルを選択』をクリックすると、画像を選択できるようになっています。

image5

HTML のレイアウトではなく、MUI のボタンを作成し、全体を統一します。

まずは、inputタグの下にボタンを作成します。

<Button variant="contained" color="primary" component="span">
  画像を選択
</Button>

inputタグにidを追加します。

また、Buttonを label タグで囲みます。

labelタグにhtmlForを設定し、inputタグのidを指定します。

inputタグをdisplay:noneで非表示にします。

<input
  id="image"
  type="file"
  accept="image/*"
  onChange={handleChange}
  style={{ display: "none" }}
/>
<label htmlFor="image">
  <Button variant="contained" color="primary" component="span">
    画像を選択
  </Button>
</label>

では、動作確認してみます。

image6

画像を追加し、Console を確認すると、

image7

画像のデータが表示されました。

このままでは、画像が選択されているか、画面では分からないので、画面に画像を表示させます。

useState で画像データの状態を管理しましょう。

const [image, setImage] = useState<File | null>()
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  if (e.target.files !== null) {
    setImage(e.target.files[0])
  }
}

次に、MUI のAvatarを使い、画像を表示する場所を作成します。

Avatarの src には、imageがある場合、URL.createObjectURLimageを指定します。

imageがない場合は、””としておきましょう。

<Box sx={{ display: "flex", justifyContent: "space-between" }}>
  <Avatar src={image ? URL.createObjectURL(image) : ""} alt="" />
  <div>
    <input
      id="image"
      type="file"
      accept="image/*"
      onChange={handleChange}
      style={{ display: "none" }}
    />
    <label htmlFor="image">
      <Button variant="contained" color="primary" component="span">
        画像を選択
      </Button>
    </label>
  </div>
</Box>

image8

では、画像を選択してみます。

image9

Avatar が設定されました。

画像が選択できたので、次は、Firebase の Storage に画像が保存できるようにします。

Firebase の Storage にアクセスします。

Rules タグをクリックします。

今のところ、ファイルの read や write が禁止されています。

image10

こちらを、認証されている場合は許可するようにします。

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth != null;
    }
  }
}

公開をクリックします。

image11

Profile.tsx へ戻ります。

以前 Firebase の初期設定した、Firebase フォルダの firebaseConfig からfirebaseAppをインポートします。

Firebase の設定は、こちらをご覧ください。

firebaseAppfirestorageを使用します。

const firestorage = firebaseApp.firestorage

handleSubmit 関数内で、try/catch を使います。

catch の場合は、エラーメッセージを表示するようにします。

const [error, setError] = useState(false)
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  event.preventDefault()
  try {
  } catch (err) {
    console.log(err)
    setError(true)
  }
}
{
  error && <Alert severity="error">送信できませんでした</Alert>
}

firebase/storageからrefをインポートします。

import { ref } from "firebase/storage"

ref の第一引数に、先程設定したfirestorage、第二引数にファイル名としてimageオブジェクトのnameを指定します。

try {
  if (image) {
    const imageRef = ref(firestorage, image.name)
  }
} catch (err) {
  console.log(err)
  setError(true)
}

firebase/storageからuploadBytesをインポートします。

import { ref, uploadBytes } from "firebase/storage"

uploadBytesの第一引数に imageRef、第二引数に image を指定します。

thenConsoleに送信内容を表示するようにします。

try {
  if (image) {
    const imageRef = ref(firestorage, image.name)

    uploadBytes(imageRef, image).then(snapshot => {
      console.log("Uploaded a file!", snapshot)
    })
  }
} catch (err) {
  console.log(err)
  setError(true)
}

では、動作確認してみます。

image12

『保存』をクリックすると、

image13

画像が送信されたようです、

Firebase の Storage を確認してみましょう。

image14

画像が保存されていました。

次回は、

ブログ一覧