React + MUIで管理画面を作ってみた

2024/08/04に公開

🤔管理画面を作るには何が適しているのか?

Flutter Webで管理画面を作っていたのですが、なんか満足できなかった。Firebase Hostingした後に、表示されるのが遅い???

WidgetにTimePickerがあるので、時計の入力画面が使えるのは気に入っていた。しかしWebの言語は、もっとUIライブラリが充実しているのではと思った💡

こちらが参考になった!
https://mui.com/x/react-date-pickers/time-picker/
https://stackblitz.com/run?file=Demo.tsx

実際に作ってみたもの

Flutter Webより表示速度は早くて、UIも綺麗な気がした。同じマテリアルデザインではあるが...

上の方:

したの方:

技術構成

  • Vite
  • TypeScript
  • React
  • Firebase
  • MUI

これだけですね。管理画面は、Flutter Webで早く作れるんですけど、でもなぜだろうか....
広く普及しているReactで作りたいと思った。

必要なライブラリの追加:

npm create vite@latest concafe-admin-dev-react -- --template react-ts
cd concafe-admin-dev-react
npm install
npm run dev

add package:

npm install @mui/material @emotion/react @emotion/styled @mui/x-date-pickers dayjs firebase

全体のコード:

// src/App.tsx
import React, { useState } from "react";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import { StaticTimePicker } from "@mui/x-date-pickers/StaticTimePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
import { initializeApp } from "firebase/app";
import {
  getFirestore,
  collection,
  addDoc,
  GeoPoint,
  Timestamp,
} from "firebase/firestore";
import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
import { FormControl, FormLabel, Grid } from "@mui/material";

// Firebaseの設定
const firebaseConfig = {
  // API KEYを設定してね
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const storage = getStorage(app);

const theme = createTheme();

function App() {
  const [shopName, setShopName] = useState("");
  const [week, setWeek] = useState("");
  const [zipcode, setZipcode] = useState("");
  const [shopInfo, setShopInfo] = useState("");
  const [phoneNumber, setPhoneNumber] = useState("");
  const [latitude, setLatitude] = useState("");
  const [longitude, setLongitude] = useState("");
  const [address, setAddress] = useState("");
  const [openTime, setOpenTime] = useState<Dayjs | null>(
    dayjs().set("hour", 9).set("minute", 0)
  );
  const [closeTime, setCloseTime] = useState<Dayjs | null>(
    dayjs().set("hour", 18).set("minute", 0)
  );
  const [imageUrl, setImageUrl] = useState("");

  const handleImageUpload = async (
    event: React.ChangeEvent<HTMLInputElement>
  ) => {
    const file = event.target.files?.[0];
    if (file) {
      const fileName = `image/${Date.now()}.${file.name.split(".").pop()}`;
      const storageRef = ref(storage, fileName);
      await uploadBytes(storageRef, file);
      const url = await getDownloadURL(storageRef);
      setImageUrl(url);
    }
  };

  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    try {
      await addDoc(collection(db, "shop"), {
        shop_name: shopName,
        week,
        zipcode,
        shop_info: shopInfo,
        phone_number: phoneNumber,
        location: new GeoPoint(Number(latitude), Number(longitude)),
        imageUrl,
        address,
        open_time: openTime?.format("HH:mm"),
        close_time: closeTime?.format("HH:mm"),
        created_at: Timestamp.now(),
        updated_at: Timestamp.now(),
      });
      alert("送信が完了しました");
    } catch (error) {
      alert(`エラーが発生しました: ${error}`);
    }
  };

  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <LocalizationProvider dateAdapter={AdapterDayjs}>
        <Container component="main" maxWidth="lg">
          <Box
            sx={{
              marginTop: 8,
              display: "flex",
              flexDirection: "column",
              alignItems: "center",
            }}
          >
            <Typography component="h1" variant="h5" gutterBottom>
              ConCafe Admin Dev
            </Typography>
            <Box
              component="form"
              onSubmit={handleSubmit}
              noValidate
              sx={{ mt: 3, width: "100%" }}
            >
              <Grid container spacing={2}>
                <Grid item xs={12} md={6}>
                  <TextField
                    required
                    fullWidth
                    id="shopName"
                    label="店舗名"
                    name="shopName"
                    autoFocus
                    value={shopName}
                    onChange={(e) => setShopName(e.target.value)}
                  />
                </Grid>
                <Grid item xs={12} md={6}>
                  <TextField
                    required
                    fullWidth
                    id="week"
                    label="営業日"
                    name="week"
                    value={week}
                    onChange={(e) => setWeek(e.target.value)}
                  />
                </Grid>
                <Grid item xs={12} md={6}>
                  <TextField
                    fullWidth
                    id="zipcode"
                    label="郵便番号"
                    name="zipcode"
                    value={zipcode}
                    onChange={(e) => setZipcode(e.target.value)}
                  />
                </Grid>
                <Grid item xs={12} md={6}>
                  <TextField
                    fullWidth
                    id="phoneNumber"
                    label="電話番号"
                    name="phoneNumber"
                    value={phoneNumber}
                    onChange={(e) => setPhoneNumber(e.target.value)}
                  />
                </Grid>
                <Grid item xs={12}>
                  <TextField
                    required
                    fullWidth
                    id="shopInfo"
                    label="店舗情報"
                    name="shopInfo"
                    multiline
                    rows={4}
                    value={shopInfo}
                    onChange={(e) => setShopInfo(e.target.value)}
                  />
                </Grid>
                <Grid item xs={12} md={6}>
                  <TextField
                    required
                    fullWidth
                    id="latitude"
                    label="緯度"
                    name="latitude"
                    type="number"
                    inputProps={{ step: "0.000001" }}
                    value={latitude}
                    onChange={(e) => setLatitude(e.target.value)}
                  />
                </Grid>
                <Grid item xs={12} md={6}>
                  <TextField
                    required
                    fullWidth
                    id="longitude"
                    label="経度"
                    name="longitude"
                    type="number"
                    inputProps={{ step: "0.000001" }}
                    value={longitude}
                    onChange={(e) => setLongitude(e.target.value)}
                  />
                </Grid>
                <Grid item xs={12}>
                  <TextField
                    required
                    fullWidth
                    id="address"
                    label="住所"
                    name="address"
                    value={address}
                    onChange={(e) => setAddress(e.target.value)}
                  />
                </Grid>
                <Grid container spacing={3}>
                  <Grid item xs={12} md={6}>
                    <FormControl>
                      <FormLabel>開店時間</FormLabel>
                      <StaticTimePicker
                        value={openTime}
                        onChange={(newValue) => setOpenTime(newValue)}
                      />
                    </FormControl>
                  </Grid>
                  <Grid item xs={12} md={6}>
                    <FormControl>
                      <FormLabel>閉店時間</FormLabel>
                      <StaticTimePicker
                        value={closeTime}
                        onChange={(newValue) => setCloseTime(newValue)}
                      />
                    </FormControl>
                  </Grid>
                </Grid>
                <Grid item xs={12}>
                  <input
                    accept="image/*"
                    style={{ display: "none" }}
                    id="raised-button-file"
                    type="file"
                    onChange={handleImageUpload}
                  />
                  <label htmlFor="raised-button-file">
                    <Button variant="contained" component="span">
                      画像をアップロード
                    </Button>
                  </label>
                </Grid>
                <Grid item xs={12}>
                  <Button
                    type="submit"
                    fullWidth
                    variant="contained"
                    sx={{ mt: 3, mb: 2 }}
                  >
                    送信
                  </Button>
                </Grid>
              </Grid>
            </Box>
          </Box>
        </Container>
      </LocalizationProvider>
    </ThemeProvider>
  );
}

export default App;

最後に

Reactの知識が必要ではありますが、生成AI使って、いい感じに仕上げました。単純な入力のみのページを作るのには、向いていそうだなと思いました。遊びで作ったので、デプロイはしてません。Firebase Hostingしたときに、表示される速度も速いはず?

Flutterで開発すれば最初から、入力フォームに使える便利なパーツは揃っていますが、UIライブラリを使えば、JavaScriptのフレームワーク、ライブラリでも1ページぐらいなら、こんなものを作れます。管理画面なら、ださくてもよいので、TailWindCSSなどを使うなりして、時間かけて作るより、便利なコンポーネントが複数用意されているMUIを使うのが、適材適所だなと思いました。

と言いながら、MUI以外で、管理画面を作るのに挑戦してみたい笑
今回は、単純に、時計のUIが欲しかったので、MUIを使用しました。

Discussion