現時点のディレクトリ構成とコード
ディレクトリ構成
/src
├── components # UIコンポーネント(再利用可能なパーツ)
│ ├── AuthButton.js # ログイン・ログアウトボタン
│ ├── AccountMenu.js # アカウントメニュー(削除ボタン含む)
│ └── ...
│
├── pages # 各ページごとのコンポーネント
│ ├── Home.js # ログイン前のホーム画面
│ ├── Login.js # ログイン画面
│ ├── Signup.js # アカウント作成画面
│ ├── Dashboard.js # ログイン後のホーム画面
│ ├── MyPage.js # マイページ
│ ├── NotFound.js # 404エラーページ
│ ├── Policy.js # プライバシーポリシー
│ ├── HowTo.js # 使い方ページ
│ ├── Terms.js # 利用規約
│ └── ...
│
├── utils # ユーティリティ関数
│ ├── firebase.js # Firebase初期設定
│ ├── auth.js # 認証関連処理(ログイン、削除)
│ └── ...
│
├── templates # UI関連
│ ├── Header.js # ヘッダー
│ ├── Footer.js # フッター
│ ├── Layout.js # レイアウト関連
│ └── ...
│
├── App.js # アプリのエントリーポイント&ルーティング関連
├── index.js # ルートレンダリング
└── ...
画面遷移一覧
| ページ名 | パス | 説明 | 遷移元・遷移先 |
|---|---|---|---|
| ホーム | / | ログイン前のトップページ | ログイン・アカウント作成へ |
| ログイン | /login | メールとパスワードでログイン | ホーム ⇄ ダッシュボード |
| アカウント作成 | /signup | アカウント新規作成フォーム | ホーム ⇄ ダッシュボード |
| ダッシュボード | /dashboard | ログイン後のメイン画面 | ログイン後に遷移 |
| マイページ | /mypage | アカウント情報の管理ページ | ダッシュボード ⇄ マイページ |
| 404ページ | *(存在しないパス) | 存在しないページのエラーページ | 不正なURLで遷移 |
- 未ログイン時の流れ
/→/login→/dashboard/→/signup→/dashboard
- ログイン後の流れ
/dashboard→/mypage
- アカウント削除後
/dashboard→/(ログアウト後にトップへ)
components:UIコンポーネント(再利用可能なパーツ)
AuthButton.js:ログイン・ログアウトボタン
import React, { useEffect, useState } from "react";
import { Button, CircularProgress } from "@mui/material";
import { auth } from "../utils/firebase";
import { GoogleAuthProvider, signInWithPopup, signOut, onAuthStateChanged } from "firebase/auth";
import { useNavigate } from "react-router-dom";
const AuthButton = () => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true); // ローディング状態を追加
const navigate = useNavigate();
useEffect(() => {
// 認証状態を監視
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
setLoading(false); // ローディング完了
});
// クリーンアップ関数
return () => unsubscribe();
}, []);
const handleLogin = async () => {
const provider = new GoogleAuthProvider();
try {
await signInWithPopup(auth, provider);
console.log("ログイン成功");
navigate("/dashboard");
} catch (error) {
console.error("ログイン失敗", error);
}
};
const handleLogout = async () => {
try {
await signOut(auth);
console.log("ログアウト成功");
navigate("/");
} catch (error) {
console.error("ログアウト失敗", error);
}
};
// ローディング中はスピナー表示
if (loading) {
return <CircularProgress />;
}
return (
<div>
{user ? (
<Button variant="contained" color="secondary" onClick={handleLogout}>
ログアウト
</Button>
) : (
<Button variant="contained" color="primary" onClick={handleLogin}>
Googleでログイン
</Button>
)}
</div>
);
};
export default AuthButton;
AccountMenu.js:アカウントメニュー(削除ボタン含む)
import React, { useState } from "react";
import { Box, Avatar, Menu, MenuItem, ListItemIcon, Divider, IconButton, Tooltip } from "@mui/material";
import { Logout, Settings, PersonAdd } from "@mui/icons-material";
import { useAuth } from "../utils/auth";
import { useNavigate } from "react-router-dom";
const AccountMenu = () => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const { user, logout, deleteAccount } = useAuth();
const navigate = useNavigate();
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleLogout = async () => {
try {
await logout();
navigate("/"); // ログアウト後にホーム画面へ遷移
} catch (error) {
console.error("ログアウトに失敗しました:", error.message);
}
};
const handleDelete = async () => {
if (window.confirm("本当にアカウントを削除しますか?")) {
try {
await deleteAccount();
navigate("/"); // 削除後にホーム画面へ遷移
} catch (error) {
console.error("アカウント削除に失敗しました:", error.message);
}
}
};
return (
<React.Fragment>
<Box sx={{ display: "flex", alignItems: "center", textAlign: "center" }}>
<Tooltip title="アカウント設定">
<IconButton
onClick={handleClick}
size="small"
sx={{ ml: 2 }}
aria-controls={open ? "account-menu" : undefined}
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
>
<Avatar sx={{ width: 32, height: 32 }}>
{user?.displayName?.charAt(0) || "U"}
</Avatar>
</IconButton>
</Tooltip>
</Box>
<Menu
anchorEl={anchorEl}
id="account-menu"
open={open}
onClose={handleClose}
onClick={handleClose}
slotProps={{
paper: {
elevation: 0,
sx: {
overflow: "visible",
filter: "drop-shadow(0px 2px 8px rgba(0,0,0,0.32))",
mt: 1.5,
"& .MuiAvatar-root": {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
"&::before": {
content: '""',
display: "block",
position: "absolute",
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: "background.paper",
transform: "translateY(-50%) rotate(45deg)",
zIndex: 0,
},
},
},
}}
transformOrigin={{ horizontal: "right", vertical: "top" }}
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
>
<MenuItem onClick={handleClose}>
<Avatar /> マイページ
</MenuItem>
<Divider />
<MenuItem onClick={handleClose}>
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
設定
</MenuItem>
<MenuItem onClick={handleLogout}>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
ログアウト
</MenuItem>
<MenuItem onClick={handleDelete}>
🗑️ アカウントを削除
</MenuItem>
</Menu>
</React.Fragment>
);
};
export default AccountMenu;
pages:各ページごとのコンポーネント
Home.js
import React from "react";
import { Container, Typography, Button, Box } from "@mui/material";
import { useNavigate } from "react-router-dom";
const Home = () => {
const navigate = useNavigate();
try {
return (
<Container sx={{ mt: 5, textAlign: "center" }}>
<Typography variant="h3" gutterBottom>
RhinoFabへようこそ!
</Typography>
<Typography variant="h5" color="text.secondary" sx={{ mb: 4 }}>
3Dモデルを管理・共有できるプラットフォーム
</Typography>
<Box sx={{ display: "flex", justifyContent: "center", gap: 3, mt: 3 }}>
<Button
variant="contained"
color="primary"
onClick={() => navigate("/login")}
sx={{ px: 5, py: 1.5, fontSize: "1.2rem" }}
>
ログイン
</Button>
<Button
variant="outlined"
color="secondary"
onClick={() => navigate("/signup")}
sx={{ px: 5, py: 1.5, fontSize: "1.2rem" }}
>
アカウント作成
</Button>
</Box>
</Container>
);
} catch (error) {
console.error("Home.jsでエラーが発生:", error);
return <div>エラーが発生しました: {error.message}</div>;
}
};
export default Home;
Login.js
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../utils/auth";
import {
Container,
Typography,
TextField,
Button,
Box,
Alert,
} from "@mui/material";
const Login = () => {
const navigate = useNavigate();
const { login, signInWithGoogle } = useAuth();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
// メールとパスワードでログイン
const handleLogin = async (e) => {
e.preventDefault();
try {
await login(email, password);
navigate("/dashboard");
} catch (error) {
setError("ログインに失敗しました。メールアドレスとパスワードを確認してください。");
}
};
// Googleログイン
const handleGoogleLogin = async () => {
try {
await signInWithGoogle();
navigate("/dashboard");
} catch (error) {
setError("Googleログインに失敗しました。");
}
};
return (
<Container maxWidth="sm" sx={{ mt: 8 }}>
<Typography variant="h4" align="center" gutterBottom>
ログイン
</Typography>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Box component="form" onSubmit={handleLogin} sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<TextField
label="メールアドレス"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
fullWidth
/>
<TextField
label="パスワード"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
fullWidth
/>
<Button type="submit" variant="contained" color="primary" fullWidth>
ログイン
</Button>
</Box>
<Box sx={{ mt: 2, display: "flex", justifyContent: "center" }}>
<Button
variant="outlined"
color="secondary"
onClick={handleGoogleLogin}
fullWidth
>
Googleでログイン
</Button>
</Box>
</Container>
);
};
export default Login;
Signup.js
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../utils/auth"; // 🔥 修正済み
import { TextField, Button, Container, Typography, Box, Alert, Modal } from "@mui/material";
const Signup = () => {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const navigate = useNavigate();
const { createUser } = useAuth(); // ✅ 修正:createUserが取得できる
const [open, setOpen] = useState(false); // モーダル制御
const handleSignup = async (e) => {
e.preventDefault();
setError(null);
if (!username || !email || !password) {
setError("すべてのフィールドを入力してください。");
return;
}
try {
await createUser(username, email, password); // 🔥 修正:createUserを呼び出し
navigate("/dashboard");
} catch (err) {
setError(err.message);
}
};
return (
<Container maxWidth="sm">
<Box sx={{ mt: 8, p: 4, boxShadow: 3, borderRadius: 2 }}>
<Typography variant="h4" align="center" gutterBottom>
アカウント作成
</Typography>
{error && <Alert severity="error">{error}</Alert>}
<form onSubmit={handleSignup}>
<TextField
label="ユーザーネーム"
placeholder="例: sekkeiya"
fullWidth
margin="normal"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<TextField
label="メールアドレス"
type="email"
placeholder="例: example@example.com"
fullWidth
margin="normal"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<TextField
label="パスワード"
type="password"
placeholder="6文字以上"
fullWidth
margin="normal"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
sx={{ mt: 2 }}
>
アカウント作成
</Button>
</form>
<Box mt={2} textAlign="center">
<Button onClick={() => navigate("/login")} variant="text">
ログインはこちら
</Button>
</Box>
{/* ✅ モーダル */}
<Modal open={open} onClose={() => setOpen(false)}>
<Box sx={{
position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)",
bgcolor: "white", boxShadow: 24, p: 4, borderRadius: 2
}}>
<Typography variant="h5">ようこそ!</Typography>
<Typography>アカウントが作成されました!</Typography>
</Box>
</Modal>
</Box>
</Container>
);
};
export default Signup;
Dashboard.js
import React from "react";
import { Box, Typography } from "@mui/material";
const Dashboard = () => {
return (
<div>
{/* メインコンテンツ */}
<Box p={3}>
<Typography variant="h4">ようこそ!</Typography>
<Typography variant="body1">
ここにダッシュボードのコンテンツが表示されます。
</Typography>
</Box>
</div>
);
};
export default Dashboard;
Mypage.js
import React, { useState } from "react";
import { Box, Button, Typography, Modal } from "@mui/material";
import { useAuth } from "../utils/auth";
const MyPage = () => {
const { logout } = useAuth();
const [open, setOpen] = useState(false);
const handleDelete = async () => {
try {
await logout();
setOpen(true);
} catch (error) {
console.error("アカウント削除失敗:", error.message);
}
};
return (
<Box sx={{ p: 4 }}>
<Typography variant="h4">マイページ</Typography>
<Button
onClick={handleDelete}
variant="contained"
color="error"
sx={{ mt: 3 }}
>
アカウント削除
</Button>
<Modal open={open} onClose={() => setOpen(false)}>
<Box sx={{
position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)",
bgcolor: "white", boxShadow: 24, p: 4, borderRadius: 2
}}>
<Typography variant="h5">アカウントが削除されました</Typography>
</Box>
</Modal>
</Box>
);
};
export default MyPage;
NotFound.js
import React from "react";
const NotFound = () => {
return (
<div>
<h1>404 - ページが見つかりません</h1>
</div>
);
};
export default NotFound;
Policy.js
import React from "react";
const Policy = () => {
return (
<div>
<h1>Privacy Policy</h1>
<p>ここにプライバシーポリシーを記載します。</p>
</div>
);
};
export default Policy;
HowTo.js
import React from "react";
const HowTo = () => {
return (
<div>
<h1>How to Use RhinoFab</h1>
<p>ここにアプリの使い方を記載します。</p>
</div>
);
};
export default HowTo;
Terms.js
import React from "react";
const Terms = () => {
return (
<div>
<h1>Terms of Service</h1>
<p>ここに利用規約を記載します。</p>
</div>
);
};
export default Terms;
templates
Header.js
import React from "react";
import { AppBar, Toolbar, Typography, Box } from "@mui/material";
import { useAuth } from "../utils/auth";
import { useNavigate } from "react-router-dom";
import AccountMenu from "../components/AccountMenu";
const Header = () => {
const { user } = useAuth();
const navigate = useNavigate();
// ✅ ロゴクリックでログイン状態に応じた遷移
const handleLogoClick = () => {
navigate(user ? "/dashboard" : "/");
};
return (
<AppBar position="static" color="primary">
<Toolbar>
{/* ✅ ロゴ部分 */}
<Typography
variant="h6"
sx={{ flexGrow: 1, cursor: "pointer" }}
onClick={handleLogoClick}
>
RhinoFab
</Typography>
{/* ✅ ユーザーメニュー(ログイン時のみ表示) */}
{user && (
<Box>
<AccountMenu />
</Box>
)}
</Toolbar>
</AppBar>
);
};
export default Header;
Footer.js
import React from "react";
import { Box, Typography } from "@mui/material";
const Footer = () => {
return (
<Box
component="footer"
sx={{
width: "100%",
height: "64px",
backgroundColor: "#f5f5f5",
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "fixed",
bottom: 0,
left: 0,
zIndex: 1000, // 他の要素より前面に表示
boxShadow: "0 -2px 4px rgba(0,0,0,0.1)",
}}
>
<Typography variant="body2" color="textSecondary">
© 2025 RhinoFab
</Typography>
</Box>
);
};
export default Footer;
Layout.js
import React from "react";
import Header from "./Header";
import Footer from "../templates/Footer";
import { Box, CircularProgress } from "@mui/material";
import { useAuth } from "../utils/auth";
const Layout = ({ children }) => {
const { loading } = useAuth();
if (loading) {
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
}}
>
<CircularProgress />
</Box>
);
}
return (
<>
{/* ✅ Headerを追加 */}
<Header />
<Box
component="main"
sx={{
minHeight: "calc(100vh - 128px)", // AppbarとFooterの高さ分を引く
paddingBottom: "64px",
}}
>
{children}
</Box>
<Footer />
</>
);
};
export default Layout;
utils:ユーティリティ関数
firebase.js:Firebase初期設定
import { initializeApp } from "firebase/app";
import {
getAuth,
GoogleAuthProvider,
signInWithPopup,
signOut,
createUserWithEmailAndPassword
} from "firebase/auth";
import {
getFirestore,
doc,
getDoc,
setDoc
} from "firebase/firestore";
// Firebase設定情報
const firebaseConfig = {
apiKey: "AIzaSyC_iO9W5qCxJRJJdJkqhpYc14G6RrHq-wI",
authDomain: "rhinofab-463be.firebaseapp.com",
projectId: "rhinofab-463be",
storageBucket: "rhinofab-463be.firebasestorage.app",
messagingSenderId: "324255802841",
appId: "1:324255802841:web:ebd4ebcf7e9571b3aa25c8",
measurementId: "G-VV3Y0SLWF8"
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const provider = new GoogleAuthProvider();
const db = getFirestore(app);
export {
auth,
provider,
db,
signInWithPopup,
signOut,
doc,
getDoc,
setDoc,
createUserWithEmailAndPassword
};
auth.js:認証関連処理(ログイン、削除)
import React, { createContext, useContext, useState, useEffect } from "react";
import { auth, provider, db } from "./firebase";
import {
signInWithEmailAndPassword,
signInWithPopup,
onAuthStateChanged,
signOut,
deleteUser,
createUserWithEmailAndPassword,
} from "firebase/auth";
import { doc, setDoc, getDoc, deleteDoc } from "firebase/firestore"; // 🔥 Firestore操作を追加
const AuthContext = createContext();
export const useAuth = () => useContext(AuthContext);
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
});
return () => unsubscribe();
}, []);
// メールとパスワードでログイン
const login = (email, password) => {
return signInWithEmailAndPassword(auth, email, password);
};
// Firestoreにユーザーデータを保存する
const saveUserToFirestore = async (user) => {
if (!user) return;
const userRef = doc(db, "users", user.uid);
const docSnap = await getDoc(userRef);
if (!docSnap.exists()) {
await setDoc(userRef, {
uid: user.uid,
displayName: user.displayName || "No Name",
email: user.email,
createdAt: new Date().toISOString(),
});
console.log("Firestoreにユーザーを追加しました!");
} else {
console.log("既にFirestoreに存在しています!");
}
};
// Googleログイン時にFirestoreにデータを保存
const signInWithGoogle = async () => {
try {
const result = await signInWithPopup(auth, provider);
const user = result.user;
await saveUserToFirestore(user);
} catch (error) {
console.error("Googleログインに失敗しました:", error.message);
throw error;
}
};
// アカウント作成関数
const createUser = async (username, email, password) => {
try {
const result = await createUserWithEmailAndPassword(auth, email, password);
const newUser = result.user;
// Firestoreにユーザー情報を保存
const userRef = doc(db, "users", newUser.uid);
await setDoc(userRef, {
uid: newUser.uid,
username: username,
email: newUser.email,
createdAt: new Date().toISOString(),
});
console.log("アカウント作成成功:", newUser);
return newUser;
} catch (error) {
console.error("アカウント作成に失敗しました:", error.message);
throw error;
}
};
// ログアウト関数
const logout = async () => {
await signOut(auth);
};
// ✅ アカウント削除機能(FirestoreとFirebase Authentication両方から削除)
const deleteAccount = async () => {
const currentUser = auth.currentUser;
if (currentUser) {
try {
// Firestoreのユーザーデータを削除
const userRef = doc(db, "users", currentUser.uid);
await deleteDoc(userRef);
// Firebase Authenticationからユーザーを削除
await deleteUser(currentUser);
setUser(null);
console.log("アカウントが削除されました。");
} catch (error) {
console.error("アカウント削除に失敗しました:", error.message);
throw error;
}
} else {
console.error("ユーザーが存在しません。");
}
};
return (
<AuthContext.Provider
value={{ user, createUser, login, signInWithGoogle, logout, deleteAccount }}
>
{children}
</AuthContext.Provider>
);
};
src
App.js:アプリのエントリーポイント&ルーティング関連
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Signup from "./pages/Signup";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import Layout from "./templates/Layout";
import { useAuth } from "./utils/auth";
const App = () => {
const { user } = useAuth();
return (
<>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/signup" element={<Signup />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={user ? <Dashboard /> : <Login />} />
</Routes>
</Layout>
</>
);
};
export default App;
index.js:ルートレンダリング
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { AuthProvider } from "./utils/auth"; // ✅ AuthProviderをインポート
import App from "./App";
import "./index.css";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<BrowserRouter>
<AuthProvider> {/* ✅ AuthProviderでラップ */}
<App />
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
);