ポートフォリオサイトのロゴ
Icon for 【React】初めてのフロントエンド〜ランダムで今日行くラーメン二郎を決めてもらう〜

【React】初めてのフロントエンド〜ランダムで今日行くラーメン二郎を決めてもらう〜

Reactを使ってランダムでラーメン二郎の店舗を表示するアプリを作成。初めてのフロントエンド開発。

はじめに

ぼく「フロントエンドなんもわからん🤮」
ということでJavaScriptすら怪しい身ですが、Reactで簡単なアプリケーションをつくってみました。

成果物案

  • ボタン押下で東京都・神奈川県のラーメン二郎の店舗情報をランダムで表示する
    • 店舗情報は店舗名・住所・営業時間・定休日とする

作成手順

とりあえずJavaScriptの基礎を知る

Pythonは触ってきたので、JavaScriptの基礎を勉強しました。

Reactの導入

最初はNext.JSのロゴがカッコよくReactの過程をすっ飛ばしてインストールしましたが、冷静に分からなさすぎたので引退しました。

node.jsをインストール後、npx create-react-app app-nameで新規プロジェクトを作成します。

Reactの基礎を知る

以下の動画でReactの基礎を学びました。

【React Hooks入門】完全初心者OK!8種類のHooksを学んでReactの理解を深めよう
新・日本一わかりやすいReact入門【基礎編】

Reactを使用する理由と各フックスの解説を一通り学ぶことができます。

ラーメン屋の情報を取得する

PythonでSeleniumを用いて取得しました。
取得したデータはCSV形式で保存しています。

取得したデータの営業時間や日付の正規化はChatGPTに丸投げです。

Firestroe/Firebaseを知る

これらの動画・記事を参考にしました。

【Firebase】FireStoreに一括でcsvまたはjson形式のデータをアップロードする
【2022最新版】Firebase入門!Reactと連携してデータベース接続をしてみよう

登録から情報の反映方法まで分かりやすく説明しているので、かなり助かりました。

導入後はCSV形式で保存したデータをJsonに変換し、こちらの記事を参考にFirebase/Firestoreにデータを一括登録します。

実際に作成してみる

最低限のCSSを設定して完成です。
CSSはこちらを参考にしました。

CSSで作るローディングアニメーション40選〜待ち時間を楽しくするテクニック

firebase.jsはもろもろの情報があるため、割愛します。

App.js
import { useEffect, useState } from 'react';
import './App.css';
import db from './firebase';
import { collection, getDocs, onSnapshot } from "firebase/firestore";
 
function App() {
  
  const [posts, setPosts] = useState([]);
  const [selectedPost, setSelectedPost] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
 
  useEffect( () => {
    const postData = collection(db, "jirouFromTokyoAndKanagawa");
    getDocs(postData).then((snapShot) => {
      const postsData = snapShot.docs.map((doc) => ({ ...doc.data()}));
      setPosts(postsData);
    });
 
    onSnapshot(postData, (post) => {
      const postsData = post.docs.map((doc) => ({...doc.data()}));
      setPosts(postsData);
    });
  }, [])
 
  const handleButtonClick = () => {
    if (isLoading) {
      return;
    }
 
    setIsLoading(true);
    setTimeout(() => {
      if (posts.length > 0) {
        const randomIndex = Math.floor(Math.random() * posts.length);
        setSelectedPost(posts[randomIndex]);
      }
      setIsLoading(false);
    }, 2000);
  }
 
  return (
    <div className="App">
      <div className="App-header">
        <h1>じろるーれっと</h1>
      </div>
      <div className='mainContaienr'>
        <table className='tableLayout'>
          {isLoading ?
            <div className='circle-body'>
              <div className='spinner-box'>
                <div className="circle-border">
                  <div className="circle-core"></div>
                </div>
              </div>
            </div>:
              selectedPost && (
                <div key={selectedPost.store_name}>
                  <h1 className='title'>{selectedPost.store_name}</h1>
                  <tr>
                    <th>店名</th>
                    <th>{selectedPost.store_name}</th>
                  </tr>
                  <tr>
                    <th>住所</th>
                    <th>{selectedPost.store_address}</th>
                  </tr>
                  <tr>
                    <th>営業時間</th>
                    <th>{selectedPost.open_time}</th>
                  </tr>
                  <tr>
                    <th>定休日</th>
                    <th>{selectedPost.close_day}</th>
                  </tr>
                </div>
              )
            }
        </table>
        <div className='button-container'>
          <div>
            <button onClick={handleButtonClick} className='randomButton'>ラーメン屋を選ぶ</button>
          </div>
        </div>
      </div>
    </div>
  );
}
 
export default App;
App.css
:root {
  --main-background-color: #1e2739;
  --main-color: white;
}
 
.App, .App-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  background-color: var(--main-background-color);
  font-size: calc(10px + 2vmin);
  color: var(--main-color);
}
 
.App {
  min-height: 100vh;
}
 
.App-header {
  position: fixed;
  z-index: 999;
  top: 0;
  left: 0;
  width: 100%;
  padding: 20px 40px;
  box-sizing: border-box;
}
 
.tableLayout, .title {
  text-align: left;
  margin: auto;
}
 
.mainContaienr {
  width: 75%;
  height: 75%;
}
 
.randomButton {
  background-color: #00af4f;
  border: none;
  color: var(--main-color);
  text-align: center;
  display: inline-block;
  font-size: 16px;
  font-weight: bold;
  padding: 10px 24px;
  border-radius: 100vh;
  margin: 50px auto 0;
  position: relative;
  transition: background-color 0.4s, transform 0.4s;
  cursor: pointer;
}
 
.randomButton:hover {
  background-color: #007f3b;
  color: white;
}
 
.randomButton:active {
	transform: translate(0,10px);
	border-bottom:none;
}
 
.spinner-box {
  width: 300px;
  height: 300px;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: transparent;
}
 
.circle-border, .circle-core {
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 50%;
}
 
.circle-border {
  width: 150px;
  height: 150px;
  padding: 3px;
  background: linear-gradient(0deg, rgba(63,249,220,0.1) 33%, rgba(63,249,220,1) 100%);
  animation: spin .8s linear 0s infinite;
}
 
.circle-core {
  width: 100%;
  height: 100%;
  background-color: var(--main-background-color);
}
 
@keyframes spin {
  from {
      transform: rotate(0);
  }
  to{
      transform: rotate(359deg);
  }
}
 
.circle-body {
  background-color: var(--main-background-color);
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  align-items: flex-start;
}

実際に決めてもらう

こんなかんじです。
どういった装飾がよいか思いつかず、ローディングとボタンだけきれいにしました。

React-App-Google-Chrome-2023-06-10-19-38-39-Trim.gif

感想

html, css, javascriptあたりをもう一度復習かな…
特にjavascriptの文法が実際に扱ってみてもまだ理解していない部分が多いので、今後とも学びつつ作成していきます。

次回はこちらで表示した住所をGoogleMapsAPIでMapを表示し、ピンを打つところまでやっていこうかと思います。