9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

実務未経験文系大学生が独学で制作したReact/firebaseのWebサイト

Last updated at Posted at 2022-12-25

こんにちは。ゆうたと申します。

タイトルの通り、プログラミング初心者の私がポートフォリオ用のwebサイトを作ったので紹介させていただきます。

それにあわせて独学の苦悩なども書きたいと思います。

わたしと同じ実務未経験でプログラミングをやっている人は共感できることも多いと思います。

この記事は、

  • only text(React/firebaseで制作したWebサイト)の紹介
  • 未経験でたった一人で制作することの苦悩

の構成で書いていきます。

この記事を読んでくださっている方のほとんどはわたしより経験や知識が豊富だと思うのでそういった方々のご指摘点があればコメントしてもらえるとうれしいです。

only text(React/firebaseで制作したWebサイト)の紹介

only text
githubはこちらです↓
https://github.com/yuutauh/first
スクリーンショット ホーム画面.png

このサイトは文字のみの投稿を重視したソーシャルメディアを目指したWebサイトです。

ホーム画面や投稿の詳細では投稿者にモザイクがしてあり、いいねをしたときにモザイクが解除され投稿者がわかるようになっています。

詳細画面
いいね画面.gif

なんでこんな仕様にしたかというと、インスタやtwitterをはじめとしたソーシャルメディアは画像やショート動画などで溢れかえっています。

投稿内容は似たり寄ったりなのに有名な人だけが評価を独り占めしている印象を受けます。

そのため文字のみで内容を判断して、ホーム画面では誰によって投稿されたかもわからないようにすればそこにソーシャルメディアとして新しさがあるのかなと思い実装しました。
  

使用した技術

:ballot_box_with_check: React
:ballot_box_with_check:Firebase

only textの画面ごとの機能

:ballot_box_with_check: デザインについて
とても悩んで探しまくった上選ばれたのはneumorphismでした。

パソコンにもスマホにも対応しています。レスポンシブデザインです。

:ballot_box_with_check: ホーム画面
ホーム画面.gif
ホーム画面では投稿されたスレッドを新しい順にソートしています。

ページネーションに関して、twitterのようにスクロールするだけで次のスレッドを表示する無限スクロールをしています。

:ballot_box_with_check: 投稿画面
投稿画面.gif
・文字列のみの投稿となります。250文字以上はバリデーションをかけています。

タグ付けタグの作成ができます。やりやすいようにタグの検索

制作したタグは自動でスレッドに付与され、投稿後はタグリストに保存されてみんなが使えるようになります。

制作したタグすでにタグリストにあるタグの名称が重複した場合は自動的にすでにタグリストにあるタグが付与されます(同じ名前のタグが何個もあるのを防ぐため)

・一つの投稿につき5つまでタグ付けできます。

匿名初回アクセス時に自動で匿名ログインされます)での投稿も可能です。

:ballot_box_with_check:タグリストタグの詳細
タグリスト.gif
投稿画面で制作されたタグの一覧が表示されています

・ホーム画面と同じく無限スクロール仕様

・タグの詳細画面は各ユーザーがタグをフォローできるようになっていて、フォロワー順、投稿が新しい順、古い順にソートできます

:ballot_box_with_check:詳細画面
詳細画面.gif
・高評価(いいね)や低評価(いやだねコメントをすることができます。

・付与したタグが表示されコメントにも高評価、低評価できます。

・hostingとfunctionsを連携してこのページのみSSRして動的にしています。

Twitterへの共有も可能です。共有内容はシンプルにスレッドの文字列と画像です。↓

:ballot_box_with_check:プロフィール画面
スクリーンショット プロフィール.png
・ユーザーのサムネはログインしたアカウントに依存します。

フォロワーフォローのユーザーリスト

フォローしたタグの表示

ユーザーが投稿したスレッドお気に入りしたスレッドの一覧

無限スクロール  などなど

*DM(ダイレクトメッセージ)機能は追加予定です...

:ballot_box_with_check:検索画面
スクリーンショット 検索画面.png
スレッド、タグ、ユーザーの検索ができます

技術面

firebaseの基本的なセットアップは割愛します。
ルーティングは以下の通りです。
react-router-domをつかってルーティングしてます。

Home.js
//ルーティング以外のコードは省略しています
import { BrowserRouter, Route, Switch } from 'react-router-dom';

const Home = () => {
return(
<BrowserRouter>
    <Switch>
    	<Route path="/body/:body">
          //詳細画面へ
    	  <ShowBubble />
        </Route>
    	<Route path="/tags/:tag">
         //タグの詳細画面へ
          <Tags />
    	</Route>
    	<Route exact path="/profile/:profile">
         //各ユーザーにプロフィール画面へ
          <Profile />
    	</Route>
    	<Route path="/search">
         //検索画面へ
          <Search /> 
    	</Route>
    	<Route path="/tagindex">
         //タグの一覧画面へ
    	  <TagIndex />
    	</Route>
    	<Route exact path="/login">
         //ログイン画面へ
          <Login />
    	</Route>
    	<Route exact path="/tos">
         //利用規約へ
    	  <Tos />
    	</Route>
    	<Route exact path="/privacy">
         //プライバシーポリシーへ
    	  <Privacy />
    	</Route>
         /*ログインしてない場合はログイン画面に遷移するようにしてますが
           匿名ログインできるため不要でした */
    	<InputRouter path="/input" component={Input} />
    	<Route path="/">
          //Feedsとなっていますがホーム画面です
          <Feeds />
    	</Route>
    </Switch>
</BrowserRouter>
)
}

Firebase Authenticationとユーザー周り

  • 先ほどのルーティングでユーザー情報取得のために工夫します。
    -> React.createContextAuthContextを作成
    -> onAuthStateChangedで取得したcurrentUserAuthContextvalueに設定
    -> HomeコンポーネントをAuthContextでラップ
    -> currnetUserAuthContext下のどの階層でも参照できるようになります
Auth.js
//Auth以外のコードは省略しています
//AuthContextを作成
export const AuthContext = React.createContext();
 
export const Auth = ({children}) => {
	const [loading, setLoading] = useState(true);
	const [currentUser, setCurrentUser] = useState(null);

	useEffect(() => {
        //ユーザー情報を取得。currentUserステートにuserを更新
		const unsubscribe = auth.onAuthStateChanged((user) =>{
            //ユーザーがログインしていない場合には匿名ログイン
			if(!user) {
				fb
				.auth()
				.signInAnonymously()
				.then((user) => {
					setCurrentUser(user);
					setLoading(false)
				})
			} else {
				setCurrentUser(user);
				setLoading(false)
			}
		})

		return unsubscribe
	}, []);

	return (
        //valueにcurrentUserを設定。
		<AuthContext.Provider value={{currentUser}}>
            //children = home.js
			{ !loading &&  children }
		</AuthContext.Provider>
	)
}

useContextについてはこちらの公式サイトを参考にしてください↓
https://ja.reactjs.org/docs/context.html

  • ログインが成功した場合、投稿やフォロワーなどユーザー情報を保存しなければならないのでfirestoreuserコレクションに保存します。
    ->signInWithPopup(googleProvider or twitterProvider)からユーザー情報を取得
    ->firestoreのsetメゾットでユーザー情報をuserコレクションに保存
    ->すでにログインしているユーザーを重複して保存しないためにmergeオプションをつけます
Login.js
//googleログインの関数
 const authWithGoogle = () => {
    fb.auth()
      .signInWithPopup(googleProvider)
      .then((user) => {
        if (!user) {
          return;
        }
        const o = {
          uid: user.user.uid,
          displayName: user.user.displayName,
          photoURL: user.user.photoURL,
        };
         //mergeオプション追加
        db.collection("users").doc(user.user.uid).set(o, { merge: true });
      })
  };
//twitterログインの関数
  const authWithTwitter = () => {
    fb.auth()
      .signInWithPopup(twitterProvider)
      .then((user) => {
        if (!user) {
          return;
        }
        const o = {
          uid: user.user.uid,
          displayName: user.user.displayName,
          photoURL: user.user.photoURL,
        };
        db.collection("users").doc(user.user.uid).set(o, { merge: true });
      })
  };

もっと上手な方法があると思います。

Firestore

スレッド及びタグのデータ構造は以下の通りです。

threads.json
{
  "threads": [
    {
      "userimage": "", //ユーザーのサムネ 
      "favorites": [],  //お気に入りしたユーザーのuidの配列
      "body": "",      //投稿された文字列
      "favoriteCount": 0,  //お気に入りされると+1
      "tagname": [],   //付与されたタグの名前の配列
      "badCount": 0,   //いやだねされると+1
      "username": "",  //投稿したユーザーの名前
      "uid": "",       //投稿したユーザーにuid
      "id": "",        //uuid()でランダムの文字列
      "bads": [],      //いやだねしたユーザーのuidの配列
      "created": { "seconds": 1665565359, "nanoseconds": 661000000 }
    },
    //以下略
  ]
tags.json
{
  "tags": [
    {
      "id": "",       //uuid()でランダムの文字列
      "userId": [],   //フォローしたユーザーのuid
      "userCount": 0,  //フォローされたら+1
      "created": { "seconds": 1664349563, "nanoseconds": 399000000 },
      "name": "",      //タグの名前
      "threadId": [],
      "threadCount": 0  //付与された投稿の個数
    },
    //以下略
  ]
}
  • inputされた文字列がbodyに格納されます。
  • 投稿画面で指定されたタグをtagnameに格納します。
  • 付与されたタグがタグリストにあるタグ or 新しく作成されるタグで挙動が分かれます。
  • タグリストにあるタグの場合、threadIdが更新されてスレッドのidが追加されます。
  • 新しく作成されるタグの場合、firestoreのtagコレクションに新しくtagが作成されます。
  • firestoreセキュリティールールのコードの公開は避けますが、threadstagsにそれぞれデータスキーマが正しいか ,正しくログインされているかユーザーがどの認証プロバイダーを利用しているか などなど設定してあります。
firestore.rules
//匿名,google,twitter認証であるか判定する
function isUserValid(auth) {
        return auth.token.firebase.sign_in_provider in ['twitter.com', 'google.com','anonymous']
}

Firestore Security Rulesについてこちらを参考にさせていただきました。

SSR(サーバーサイドレンダリング)

  • firebase hostingcloud functionsを連携させて/body/{bodyId}のパスにリクエストが来た場合、returnHtmlWithOGPをよびだしています。
  • /body/{bodyId}のパスは投稿の詳細画面になっています。
  • 公式にもやり方があるので参考までに↓

firebase.json
"hosting": {
     //省略
    "rewrites": [
      {
        "source": "/body/*",
        "function": "returnHtmlWithOGP"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  },
functions/index.js
exports.returnHtmlWithOGP = functions.https.onRequest((req, res) => {
	res.set('Cache-Control', 'public, max-age=300, s-maxage=600')
	// Access URL '/body/{bodyId}'
	const [, , bodyid] = req.path.split('/')
	const domain = 'https://onlytext.net'
	let indexHTML = fs.readFileSync('./hosting/index.html').toString()
  
	db.collection('threads').doc(bodyid).get()
		.then((snapshot) => {
		  const thread = snapshot.data()
		  let body = thread.body
		  indexHTML
			.replace(/\<title>.*<\/title>/g, '<title>' + body + '</title>')
			.replace(/<\s*meta name="description" content="[^>]*>/g, '<meta name="description" content="' + body + '" />')
			.replace(/<\s*meta property="og:title" content="[^>]*>/g, '<meta property="og:title" content="' + body + '" />')
			.replace(/<\s*meta property="og:url" content="[^>]*>/g, '<meta property="og:url" content="' + domain + '" />')
			.replace(/<\s*meta property="og:description" content="[^>]*>/g, '<meta property="og:description" content="' + body + '" />')
			.replace(/<\s*meta name="twitter:card" content="[^>]*>/g, '<meta name="twitter:card" content="summary_large_image" />')
		  res.status(200).send(indexHTML)
		})
		.catch(err => {
		  res.status(404).send(indexHTML)
		})
})

SSRについてはこちらの記事を参考にさせていただきました

検索機能

  • firebaseで検索するには外部エンジンが必要でお金がかかるらしいのでそれらを避ける形にしました。
  • 外部エンジンを使っていないので検索ワードの最初の文字が一致しないとダメな大変厳しい仕様。
searchProfile.js
const SearchProfile = ({ index, setIndex }) => {
  const [search, setSearch] = useState("");
  const [result, setResult] = useState([]);
  const [error, setError] = useState("");
   //省略

  const SearchTag = (term) => {
    if (typeof term == "string") {
      const searchTerm = term.toLowerCase();
      const strlength = searchTerm.length;
      const strFrontCode = searchTerm.slice(0, strlength - 1);
      const strEndCode = searchTerm.slice(strlength - 1, searchTerm.length);
      const endCode =
        strFrontCode + String.fromCharCode(strEndCode.charCodeAt(0) + 1);

      db.collection("users")
        .where("displayName", ">=", searchTerm)
        .where("displayName", "<", endCode)
        .limit(10)
        .get()
        .then((docs) => {
          if (docs.size == 0) {
            setError("該当するユーザーがいません...");
            setResult([]);
          } else {
            const items = [];
            docs.forEach((doc) => {
              items.push(doc.data());
            });
            setResult(items);
            setError("");
          }
        });
    }
  };
  return(
   <>
     //省略

     <input
      placeholder="user を検索"
      type="text"
      value={search}
      onChange={(e) => {
        setSearch(e.target.value);
      }}
      />
        <button onClick={() => { SearchTag(search) }}>
          <i className="uil uil-search"></i>
        </button>
  </>
)
}

無限スクロール

反省点

  • バズる要素がなにもないこと。
  • Reactをあまり生かせていない、理解しきれていない(コンポーネントにすべきところを何度も同じ処理をかいてしまっている)
  • 変数、関数、CSSの命名が雑でコードの訂正が大変。他人が見たら絶対にわからないです。
  • アニメーションがない
    などなど

一人で制作することの苦悩

  • 決まらないデザイン
    欲をかいて一向にすすまない
    figmaやdribbble、pinterest を参考して、いざ自分のサイトに実装してみると、なんかピンと来ない。ピンとくるまで自分でなんとか修正する。
    →さらに良さそうなのがあったこれにしよう
    →いざ実装、なんかピンと来ない、修正
    →さらに良さそう…
    の無限ループ
    CSSを忘れるのでその度に検索
    どこかであきらめるしかないです

  • 実装したい仕様の検索が大変
    例えば、投稿された内容をtwitterに共有したいとします。
    そこでtwitter 共有などなど色々検索するわけですが、なかなか欲しい情報が手に入りません。
    この場合twitterに限らずSNSに投稿をシェアしたい場合はOGPの設定をすることで実装できOGPと検索することで欲しい情報が手に入るわけですが、
    OGPという単語に行き着くまでが独学だととても大変です(調べてから実際にコードを書くのも大変ですが)
    私の場合、
    firebase関連の言葉(今でも)
    ソート
    無限スクロール
    初回アクセス時のモーダルのポップアップ
    アニメーション
    などなど
    頭の中でなんとなく実装のイメージ出来てるのに、なんと検索していいのかわからない
    このもどかしさは初心者には厳しいものがあります

  • どうしても限界がある

youtubeで紹介されたとおりにコーディングしてもうまくいかないことがあります。

nodeはライブラリの変化が激しいことが原因らしいです。すでに使えないライブラリで紹介されていたらコーディングしても時間の無駄になりますし、そもそも使えないライブラリであるかの判断も厳しいです。

うまくいかないコードだと判断したら諦めて他を探すことが重要だと感じました。

おわりに

最後まで読んでいただきありがとうこざいます。

冒頭にも書きましたがなにかご指摘などがあればコメントしてください。

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?