⏰
React + MUIで管理画面を作ってみた
🤔管理画面を作るには何が適しているのか?
Flutter Webで管理画面を作っていたのですが、なんか満足できなかった。Firebase Hostingした後に、表示されるのが遅い???
WidgetにTimePickerがあるので、時計の入力画面が使えるのは気に入っていた。しかしWebの言語は、もっとUIライブラリが充実しているのではと思った💡
こちらが参考になった!
実際に作ってみたもの
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