🐣

知識ゼロからWEB開発までの道 vol.3 【記事の投稿】

2024/12/02に公開

はじめに

この記事では、React知識ゼロの筆者が、Reactを用いたWEBサービスを開発するまでの手順を公開しています。
筆者のプログラミングスキルはPythonを高専教育で習得したレベルです。
特別な知識はありません。

過去の投稿はこちら⬇︎
https://zenn.dev/acntechjp/articles/ec9beb5521ef4a

https://zenn.dev/acntechjp/articles/414f2f496ea566

記事の投稿フォーム作成

前回の認証機能を利用して、記事の投稿フォームを作成します。
指定したユーザーのみ投稿を可能にするよう、認証制限をかけます。
DBはFirebaseのFirestoreを利用します。

ReactのアプリでFirebaseをバックエンドとした記事投稿機能を構築したいです。
要件に従って初心者にもわかりやすくフォルダ構成を明確にして丁寧に教えて下さい。
## 要件
1. Informationに、投稿した記事を表示させる。
2. Informationページの右上に記事投稿のボタンを設置して、新規投稿ページに遷移できるようにする。
3. 新規投稿が可能なのは指定するGoogleアカウントユーザのみである。
4. Informationページに出力されるのは「タイトル」、「内容」、「写真」、「投稿日時」である。

※以下ChatGPT4oの回答を引用

フォルダ構成

my-app/
├── src/
│   ├── components/
│   │   ├── Header.js          // ヘッダー(ナビゲーションバー含む)
│   │   ├── ArticleCard.js     // 記事表示カード
│   ├── pages/
│   │   ├── Information.js     // 記事一覧ページ
│   │   ├── AddArticle.js      // 新規投稿ページ
│   ├── context/
│   │   ├── AuthContext.js     // 認証管理
│   ├── firebase/
│   │   ├── firebase-config.js // Firebase設定
│   ├── App.js                 // アプリ全体のルーティング
│   ├── index.js               // Reactエントリーポイント

構築手順

1. Firebase プロジェクトのセットアップ

  1. Firebase Consoleでプロジェクトを作成。
  2. Authentication:
    • サインイン方法に「Google」を有効化。
  3. Firestore:
    • 「articles」というコレクションを作成。
    • フィールド:
    • title: 記事のタイトル (string)
    • content: 記事内容 (string)
    • photoURL: 写真URL (string)
    • createdAt: 投稿日時 (timestamp)
    • author: 投稿者のUID (string)

2. Firebase 設定ファイル

src/firebase/firebase-config.js

firebase-config.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_AUTH_DOMAIN",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_STORAGE_BUCKET",
  messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
  appId: "YOUR_APP_ID",
};

// Firebaseアプリ初期化
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app); // 認証
export const db = getFirestore(app); // Firestore

3. 認証管理コンテキスト

指定したGoogleアカウントのみ新規投稿を許可するための認証ロジックをAuthContext.jsで実装。
src/context/AuthContext.js

AuthContext.js
import React, { createContext, useState, useEffect } from "react";
import { onAuthStateChanged, signInWithPopup, GoogleAuthProvider } from "firebase/auth";
import { auth } from "../firebase/firebase-config";

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [currentUser, setCurrentUser] = useState(null);
  const [isAdmin, setIsAdmin] = useState(false);

  const adminEmail = "admin@example.com"; // 管理者のGoogleアカウント

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setCurrentUser(user);
      if (user?.email === adminEmail) {
        setIsAdmin(true);
      } else {
        setIsAdmin(false);
      }
    });

    return () => unsubscribe();
  }, []);

  const signInWithGoogle = async () => {
    const provider = new GoogleAuthProvider();
    await signInWithPopup(auth, provider);
  };

  return (
    <AuthContext.Provider value={{ currentUser, isAdmin, signInWithGoogle }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

4. Informationページ

src/pages/Information.js

Information.js
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { collection, query, orderBy, onSnapshot } from "firebase/firestore";
import { db } from "../firebase/firebase-config";
import ArticleCard from "../components/ArticleCard";

const Information = () => {
  const [articles, setArticles] = useState([]);

  useEffect(() => {
    const q = query(collection(db, "articles"), orderBy("createdAt", "desc"));
    const unsubscribe = onSnapshot(q, (snapshot) => {
      setArticles(snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() })));
    });

    return () => unsubscribe();
  }, []);

  return (
    <div>
      <h1>記事一覧</h1>
      <Link to="/add-article">
        <button>記事を投稿</button>
      </Link>
      <div>
        {articles.map((article) => (
          <ArticleCard key={article.id} article={article} />
        ))}
      </div>
    </div>
  );
};

export default Information;

5. 新規投稿ページ

src/pages/AddArticle.js

AddArticle.js
import React, { useState } from "react";
import { useContext } from "react";
import { collection, addDoc, serverTimestamp } from "firebase/firestore";
import { db } from "../firebase/firebase-config";
import AuthContext from "../context/AuthContext";

const AddArticle = () => {
  const { isAdmin, currentUser } = useContext(AuthContext);
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [photo, setPhoto] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!isAdmin) {
      alert("投稿権限がありません");
      return;
    }

    try {
      const newArticle = {
        title,
        content,
        photoURL: photo || "",
        createdAt: serverTimestamp(),
        author: currentUser.uid,
      };

      await addDoc(collection(db, "articles"), newArticle);
      alert("記事が投稿されました!");
    } catch (error) {
      console.error("記事の投稿に失敗しました:", error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <h1>新規記事投稿</h1>
      <input
        type="text"
        placeholder="タイトル"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        required
      />
      <textarea
        placeholder="内容"
        value={content}
        onChange={(e) => setContent(e.target.value)}
        required
      />
      <input
        type="text"
        placeholder="写真URL"
        value={photo}
        onChange={(e) => setPhoto(e.target.value)}
      />
      <button type="submit">投稿</button>
    </form>
  );
};

export default AddArticle;

6. 記事カード

src/components/ArticleCard.js

ArticleCard.js
import React from "react";

const ArticleCard = ({ article }) => (
  <div>
    <h2>{article.title}</h2>
    <p>{article.content}</p>
    {article.photoURL && <img src={article.photoURL} alt={article.title} />}
    <p>投稿日時: {new Date(article.createdAt?.toDate()).toLocaleString()}</p>
  </div>
);

export default ArticleCard;

完成後の動作

  1. Information.js:
    • 投稿された記事が「タイトル」「内容」「写真」「投稿日時」とともに表示されます。
    • 右上に投稿ボタンがあり、新規投稿ページに遷移可能。
  2. AddArticle.js:
    • 管理者Googleアカウントのみ投稿可能。
  3. リアルタイム更新:
    • FirestoreのonSnapshotにより、記事一覧がリアルタイムで更新されます。

これで要件を満たした記事投稿アプリが完成します!

実装してみた

ひとまず上記手順をベースに構築して、その後エラー修正と希望するレイアウトに修正していきました。
特に、ページ遷移のコードは生成されていないのでApp.jsに追加しました。

☝️これも、聞いたら答えてくれたのでわからなかったら聞いてみて下さい。

完成した投稿フォーム

  1. ログインなし(投稿ボタン非表示)
  2. 指定アカウントでログインあり(投稿ボタン表示)
  3. 新規投稿ページに遷移
  4. タイトルと内容を入力して投稿ボタンを押下
    内容はChatGPT作(若干宗教味を感じる文言ですが、まあ良いこと言ってますね。)
  5. 記事が一覧に表示されていることを確認
  6. 指定していない別のアカウントでログイン(投稿ボタン非表示)

ページネーションや投稿内容の改行にも対応させたいなと思いつつ、機能的なところは完成したのでとりあえずよしとします。

最後に

これを始めた当初、Reactの基礎から全く知らなかったですがトライアンドエラーを繰り返していたら何故か何がどう動いているのかわかってくるものですね。
もちろんまだまだわからないことだらけですが。
ただ、こうやって自分の中で仮説と立証を繰り返し、興味を持って調べるのが技術の習得に一番だと私は思います。
なんでも楽しく学ぶのが一番です。

デザイン的なところは始めるとキリがないので、別途番外編ということで、メイン作成に疲れたら手をつけようかと思います。
次回はメインである受注管理システムの構築です。
最初は注文の受付フォーム作成からで。

過去の投稿

Accenture Japan (有志)

Discussion