架空レストランHPの制作過程 Part5 Reactでコーディング編

前回の記事はこちら

https://qiita.com/yamashee/items/d9962e2b8df8288b2157

### 環境構築

ビルドツールは[Vite](https://vitejs.dev/)を使用しました。

Viteは従来のビルドツールに比べて簡単にビルドできるのが魅力の一つです。

コマンドプロンプトを開いて以下のコマンドを実行します。`project-name`には、任意の名称を設定します。
```bash
npm create vite@latest project-name
```

すると、以下のようにフレームワークの選択肢が出るので**React**を選びます。
```
? Select a framework
    Vanilla
    Vue
    React
    Preact
    Lit
    Svelte
    Solid
    Qwik
    Others
```

すると、記述方式の選択肢が出てきます。今回は普通のJavaScriptを選びました。

```
? Select a variant: 
    TypeScript
    TypeScript + SWC
    JavaScript
    JavaScript + SWC
    Remix 

```

これだけで、Reactで開発できる環境を作ってくれます。

`cd project-name`で作成したディレトリに移動して、`npm install`で、現ディレクトリにある `package.json`に記述されているパッケージがインストールされます。

これで環境構築は完了です。
`npm run dev`で開発サーバーを起動し、ブラウザで[http://localhost:5173/](http://localhost:5173/)を開くとこのような初期画面が表示されます。
![firstView.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1902958/f320b2af-c0a3-231f-8ddc-b6cb0061390a.jpeg)

## HTMLコーディング
![directory.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1902958/cadbbe6f-16ae-b672-86fe-6d0dcd8cbb70.jpeg)

環境開発すると、上記のようなディレクトリ構造になっています。
App.jsxが初期画面のコードが記載されているファイルです。

App.jsxを開き、`return ()`の中に記載されてあるコードを消して、ここに今回作成するページのhtmlをバリバリ書いていきます。

本来であればヘッダーで1コンポーネント、見出しで1コンポーネント、みたいな感じでコンポーネントを分けるのがReactの標準的手法なのですが、今回作成するのはTOPページ1ページのみで、その場合、コンポーネントを細かく分ける旨味は少ないので殆ど同じファイル内に収めます。
<details><summary>App.jsxの記述はこちら</summary>

```jsx:App.jsx
function App() {

  return (
    <>
      <header>
        <h1><a href="#"><img src="./images/lafesta.webp" alt="イタリアンレストランLa Festa" /></a></h1>
        <div>
          <button className="reserveButton">予約</button>
          <button className="menuButton">
            <img src="./images/menu.svg" alt="menu" />
          </button>
        </div>
        {/* .menuButtonクリックでナビゲーションを開く */}
      </header>

      <main>
        <div className="main-inner">

          <div className="fv">
            <img className='fv-img' src="./images/kv.webp" alt="" />
            <p className="fv-copy">
              イタリアの食の冒険へと、私たちシェフがご案内します。
            </p>
          </div>

          <section className="concept" id='concept'>
            <h2>コンセプト</h2>
            <p>イタリアンレストラン『La Festa』は、イタリアの古き良き食文化と、先進的な価値観を取り入れたイタリアンレストランです。<br />
              本場イタリアで5年の修行を積んだシェフが、2013年に開業しました。<br />
              こだわりのキッチンで、最高の食材を、熟練のスキルを持ったシェフ達が心を込めて調理しています。<br />
              内装は、歴史的建造物が立ち並ぶイタリアのミラノを彷彿とさせる雰囲気。都会の喧騒の中で、ミラノの美しい雰囲気を感じつつ、優雅なひと時をお過ごしください。</p>
          </section>

          <section className="news" id='news'>
            <h2>更新情報</h2>
            {/* 更新情報コンテンツ */}
          </section>

          <section className="dinner" id='dinner'>
            <h2>ディナー</h2>
            <div className="dinner-menu">
              <div className="dinner-menu-text">
                <h3>自家製窯で焼いた特性ピザ</h3>
                <p>『La festa』自家製の特性石窯で焼き上げ、イタリアの伝統的な味に当店独自のテイスティングを加えた特別なピザです。<br />
                  外側はよりクリスピーに、内側はよりふんわりと、他では味わえないスモーキーな味わいに仕上がっています。<br />
                  生地が膨らみ黄金色に輝き、チーズがとろとろに溶け、一口食べるごとに風味が溢れるピザをお楽しみください。</p>
              </div>
              <img src="./images/pizza.webp" alt="自家製窯で焼いた特性ピザ" />
            </div>
            <div className="dinner-menu">
              <div className="dinner-menu-text">
                <h3>定番ミネストローネ</h3>
                <p>開店当初から、シーズン通して変わらぬ味を維持する『La Festa』の定番ミネストローネです。ご堪能できます。<br />完熟トマト、みずみずしいパプリカ、甘みのある玉ねぎは契約農家直送です。<br />これらの新鮮な野菜の旨味を最大限に引き出すため、時間をかけてじっくりと煮込んでいます。</p>
              </div>
              <img src="./images/minestrone.webp" alt="定番ミネストローネ" />
            </div>
            <div className="dinner-menu">
              <div className="dinner-menu-text">
                <h3>ワイン</h3>
                <p>当店で扱うワインは、全てシェフ自ら現地で試飲し、納得のいくものだけを厳選しております。<br />定番の銘柄から、日本ではなかなか手に入らない希少なものまで、幅広くご用意しておりますので。<br />お料理との相性はもちろんのこと、お客様の好みに合わせてワインをペアリングすることも可能です。<br />お客様がお気に入りのワインと出会っていただけることを願っています。</p>
              </div>
              <img src="./images/wine.webp" alt="ワイン" />
            </div>
            <a href="#" className="link">ディナメニューを詳しく見る</a>
          </section>

          <section className="lunch" id='lunch'>
            <h2>ランチ</h2>
            <p>リーズナブルなお値段で、『La Festa』の料理をお楽しみいただけます。<br />
              おひとり様からでもお気軽に来店しやすいよう、カウンター席に仕切りを設けています。<br />
              ランチ帯もお酒の提供を行っています。<br />
              お手軽な贅沢時間をゆっくりとお過ごしください。</p>
            <h3>日替わりイタリアンランチ</h3>
            {/* ランチの画像群 */}
            <a href="#" className='link'>ランチメニューを詳しく観る</a>
          </section>

          <section className="environment" id='environment'>
            <h2 className='h2-green'>環境への取り組み</h2>
            <p>イタリアでは環境問題への関心が高く、特に食に関しては高い意識を持った人が多いです。BIOや生産者認証のある食材、小規模生産者からの購買を心がける、食材を無駄にしない『Zero
              waste』が注目されています。<br />
              『La Festa』もそれに共感し、環境配慮に努めています。</p>
            <div className="environment-actions">
              <div className="environment-action">
                <h3 className='h3-green'>エコフレンドリーな水曜日</h3>
                <p>『La Festa』では毎週水曜日をエコフレンドリーな日として、牛肉の提供を停止し、植物性由来を食材を中心に提供しています。水曜日ならではの特別なメニューをお楽しみください。</p>
                <a href="#" className='link'>水曜日限定のメニューはこちら</a>
                <img src="./images/vegetable.webp" alt="毎週水曜日は植物性由来の食品を中心に提供しています。" />
              </div>
              <div className="environment-action">
                <h3 className='h3-green'>食べ残しのお持ち帰り推奨</h3>
                <p>外食産業における食べ残しによるフードロスは相当量を占めています。当店では、お持ち帰り用の容器をご用意しています。</p>
                <p><small>※持ち帰り食中毒などのリスクを伴います。自己責任でお願いいたします。また、お持ち帰りいただいた料理はできるだけ早くお召し上がりください。</small></p>
                <img src="./images/takeaway.webp" alt="食べ残し防止のために、お持ち帰り用容器を提供しています。" />
              </div>
              <div className="environment-action">
                <h3 className='h3-green'>仕込み料理の廃棄対策</h3>
                <p>『La Festa』は売れ残った料理を、ユーザーがお得に食べられるサービス『TABETE』を導入しています。
                  『TABETE』経由で料理をご注文いただいた場合、通常の約4割引で料理を提供させていただきます。</p>
                <a href="#" target="_blank" rel="noreferrer noopener" className='link'>『TABETE』について詳しく見る</a>
                <img src="./images/tabete.webp" alt="TABETE" />
              </div>
            </div>
          </section>

          <section className="chefs" id="chefs">
            <h2>シェフ達</h2>
            <p>各店舗に経験実力豊富なシェフが心を込めて調理しています。</p>
            <div className="chef">
              <h3>新宿本店オーナーシェフ<br />北条 剛</h3>
              <img src="./images/tsuyoshi.webp" alt="" />
              <div className='chef-profile'>
                <h4>profile</h4>
                <p>1977年長野県生まれ。中学時代、イタリア旅行の際食べた料理に感銘を受け、料理人の道を志す。1997年イタリアに渡り、で料理を学び、伝統的な調理法と革新的なアイデアを融合させた料理を学ぶ。2003年に帰国後、都内の3つ星レストラン『ラ・ステラ』でキッチン担当として腕を磨く。2008年に独立し、故郷である長野でレストラン『リストランテ イル・ソーニョ』を開業。2012年、後継者に店を譲り、2013年に新宿に『La Festa』をオープン。2017年に国際的料理コンテスト『クチーナ・デル・モンド』で優勝。</p>
              </div>
            </div>
            <div className="chef">
              <h3>池袋店オーナーシェフ<br />藤川 真琴</h3>
              <img src="./images/makoto.webp" alt="" />
              <div className='chef-profile'>
                <h4>profile</h4>
                <p>1984年栃木県生まれ。高校卒業後、都内の料理専門学校でイタリアン料理と店舗経営に関する知識を学ぶ。『ホテルレストラン・グランシエル』、『トラットリア・ヴェネツィア』を経て、2012年に『リストランテ・イル・ケ・カンタ』で料理長を務める。2018年に『La Festa池袋店』のオーナシェフに就任。『La Festa』の多店舗展開に貢献する傍ら、料理教室や講演活動など幅広く活動中。</p>
              </div>
            </div>
            <div className="chef">
              <h3>錦糸町店オーナーシェフ<br />Sofia Russo</h3>
              <img src="./images/russo.webp" alt="" />
              <div className='chef-profile'>
                <h4>profile</h4>
                <p>1991年ローマ生まれ。幼少期から料理に親しみ、家族や友人に料理を振る舞う傍ら。日本のポップカルチャーに興味を持ち、大学では日本語を専攻。2015年から来日し、ローマ在住時に知り合った、日本でイタリアン料理店を営む友人のもとで5年間修行を積む。2020年、新たな挑戦を求め、『La Festa錦糸町店』のオープニングスタッフとして厨房に立つ。本場イタリアで培った技と感性を活かしつつ、日本人の好みに合わせたテイスティングを得意とする。</p>
              </div>
            </div>
          </section>

          <section className="overview" id='overview'>
            <h2>店舗概要</h2>
            {/* 店舗概要テーブル */}
          </section>

          <section className="questionSection" id='questionSection'>
            <h2>よくあるご質問</h2>
            {/* アコーディオン✕3 */}
          </section>

          <section className="recruit" id='recruit'>
            <h2>リクルート</h2>
            <p>『La・Festa』は、ホールスタッフとキッチンスタッフを募集しています。<br />
              当レストランは、ホールもキッチンも、妥協のないサービス提供を心掛けています。<br />
              高いレベルのサービスを追求する仲間と共に切磋琢磨し、自身のスキルを磨くことができます。<br />
              成長意欲のある方には、責任ある仕事を任せることで、更なるキャリアアップの機会を提供します。<br />
              お客様へのサービス精神と高いホスピタリティを備え、常に最高の体験を提供する向上心を持つ方は、ぜひお問い合わせください。</p>
            <a href='#faq'>リクルートに関するお問い合わせはこちら</a>
            <img src="./images/recruit.webp" alt="ホールスタッフとキッチンスタッフを募集しています。" />
          </section>

          <section className="instagram" id='instagram'>
            <h2>Instagram</h2>
            {/* Instagramの直近6投稿分くらい */}
            <a href="#" className="instagram-account" target="_blank" rel="noreferrer noopener">
              <p>@lafesta_italian</p><img src="./images/instagram.webp" alt="instagram" />
            </a>
          </section>

          <section className="faq" id='faq'>
            <h2>お問い合わせ</h2>
            <form action="your_form_action.php" method="post">
              <div className="form-group">
                <label htmlFor="category">お問い合わせカテゴリ</label>
                <select id="category" name="category" required>
                  <option value="reservation">ご予約について</option>
                  <option value="feedback">ご感想</option>
                  <option value="food">料理</option>
                  <option value="beverage">ビバレッジ</option>
                  <option value="interior">店内</option>
                  <option value="environment">環境への取り組み</option>
                  <option value="payment">お支払い</option>
                  <option value="lost_and_found">お忘れ物</option>
                  <option value="recruit">リクルート</option>
                  <option value="other">その他</option>
                </select>
              </div>
              <div className="form-group">
                <label htmlFor="store">店舗</label>
                <select id="store" name="store">
                  <option value="">選択してください</option>
                  <option value="shinjuku">新宿本店</option>
                  <option value="ikebukuro">池袋店</option>
                  <option value="kinshichoo">錦糸町店</option>
                </select>
              </div>
              <div className="form-group">
                <textarea id="content" name="content" rows="10" placeholder="こちらにお問い合わせ内容を入力してください。" required></textarea>
              </div>
              <button type="submit">送信</button>
            </form>
          </section>

        </div >
      </main >

      <footer>
        <div className="footer-inner">
          <div className="footer-top">
            <div className="footer-logoWrap"><img src="./images/lafesta.webp" alt="イタリアンレストランLa Festa" /></div>
            <button className="reserveButton reserveButton--footer">ご予約</button>
          </div>
          {/* ナビゲーション */}
          <small>※このWebサイトはフィクションです</small>
        </div>
      </footer>

    </>
  )
}

export default App
```
</details>

動的な実装が必要そうな部分を除いてコーディングしました。

ちなみにReactでhtmlタグを記述する際、`class`は`className`と記載します。
また、`<br>`や`<img>`など終了タグがないタグに関しては`<br />`や`<img />`のように末尾を` />`にする必要があるので注意してください。

## ナビゲーション
ナビゲーションはPC用ヘッダーに一つ、ハンバーガーメニューに一つ、フッターに一つと、計3箇所必要なので`App.jsx`のナビゲーションを入れたい箇所に`<Navigation />`と記述し、コンポーネントを呼び出すようにします。

この際、`App.jsx`上部に`import Navigation from './Navigation';`を記載しないと、うまく読み込まれないため、忘れないよう注意してください。

```jsx:Navigation.jsx
const Navigation = () => {
  return (
    <ul className="menu">
      <li>
        <a href="#faq">お問い合わせ</a>
      </li>
      <li>
        <a href="#overview">店舗概要</a>
      </li>
      <li>
        <a href="#concept">コンセプト</a>
      </li>
      <li>
        <a href="#news">更新情報</a>
      </li>
      <li>
        <a href="#dinner">ディナー</a>
      </li>
      <li>
        <a href="#lunch">ランチ</a>
      </li>
      <li>
        <a href="#environment">環境への取り組み</a>
      </li>
      <li>
        <a href="#questionSection">よくあるご質問</a>
      </li>
    </ul>
  );
};

export default Navigation;
```

## よくあるご質問
アコーディオンの実装には`hdetails`タグと`summary`タグを使用しました。
これにより、一応はcssを記載せずとも最低限のアコーディオンUIは実装できます。

ただ、これらのタグを使った場合だと、よくあるスライドして出てくるようなアニメーションのUIにならないのが少し残念なところ。。

```jsx:Question.jsx
const Question = () => {
    
    return (
        <div className="accordion">
            <details>
                <summary className="question"><span>Q. 14:00〜18:00は営業していますか？</span><img src="./images/vector.svg" alt="" /></summary>
                <p className="answer">A. 14:00〜18:00は営業時間外となっております。なお、当時間内のお電話は受け付けております。</p>
            </details>
            <hr />
            <details>
                <summary className="question"><span>Q. 来店時はドレスコード必須ですか？</span><img src="./images/vector.svg" alt="" /></summary>
                <p className="answer">A. 来店時のドレスコードは必須ではありません。ただ、他のお客様をご尊重いただき、最低限清潔感のある服装でお越しください</p>
            </details>
            <hr />
            <details>
                <summary className="question"><span>Q. 食べ残しは必ず持ち帰る必要がありますか？</span><img src="./images/vector.svg" alt="" /></summary>
                <p className="answer">A. 食べ残しのお持ち帰りは必須ではありません。提供する料理の中には、長時間の保存が難しいものもあります。お持ち帰り後、できるだけ早めにお召し上がりいただける際のみ、お持ち帰りいただければと思います。</p>
            </details>
            <hr />
            <details>
                <summary className="question"><span>Q. 喫煙可能ですか？</span><img src="./images/vector.svg" alt="" /></summary>
                <p className="answer">A. 当店は電子タバコ含め、全席禁煙となっております。あらかじめご了承ください。</p>
            </details>
        </div>
    )
}

export default Question
```

## 店舗概要
店舗ごとに表示項目を切り替える、いわゆる「タブ切り替え」UIの実装です。

まず、各店舗のデータセット`shopOverview.js`を作成します。
```js:shopOverview.js
export const shopOverview = {
    'shinjuku': {
        name: 'イタリアンレストラン La Festa 新宿本店',
        image: './images/shinjuku-vibe.webp',
        chair: '213',
        inquiry: '000-0000-000',
        access: '東京都新宿区○○○○',
    },
    'ikebukuro': {
        name: 'イタリアンレストラン La Festa 池袋店',
        image: './images/ikebukuro-vibe.webp',
        chair: '195',
        inquiry: '111-1111-1111',
        access: '東京都豊島区○○○○',
    },
    'kinshicho': {
        name: 'イタリアンレストラン La Festa 錦糸町店',
        image: './images/kinshicho-vibe.webp',
        chair: '201',
        inquiry: '222-2222-2222',
        access: '東京都墨田区○○○○',
    }
}
```
`shopOverview.js`をimportし、店舗概要のjsxを作成します。
```jsx:Overview.jsx
import { useState } from 'react'
import { shopOverview } from './shopOverview'

const Overview = () => {
    const [selectedShop, setSelectedShop] = useState('shinjuku')
    const handleSelectShop = (e) => {
        setSelectedShop(e);
    }

    return (
        <div>
            <div className="overview-buttons sp-only">
                <button onClick={() => handleSelectShop('shinjuku')} className={selectedShop === 'shinjuku' ? 'isActive' : ''}>新宿本店</button>
                <button onClick={() => handleSelectShop('ikebukuro')} className={selectedShop === 'ikebukuro' ? 'isActive' : ''}>池袋店</button>
                <button onClick={() => handleSelectShop('kinshicho')} className={selectedShop === 'kinshicho' ? 'isActive' : ''}>錦糸町店</button>
            </div>
            <img src={shopOverview[selectedShop].image}></img>
            <table>
                <tr>
                    <th>店名</th>
                    <td>{shopOverview[selectedShop].name}</td>
                </tr>
                <tr>
                    <th>営業時間</th>
                    <td>
                        ランチ 11:40～14:00<br />
                        ディナー 18:00～23:00
                    </td>
                </tr>
                <tr>
                    <th>定休日</th>
                    <td>毎週月曜日</td>
                </tr>
                <tr>
                    <th>席数</th>
                    <td>{shopOverview[selectedShop].chair}席</td>
                </tr>
                <tr>
                    <th>問い合わせ</th>
                    <td>
                        <p>電話：<a href={`tel:${shopOverview[selectedShop].inquiry}`}>{shopOverview[selectedShop].inquiry}</a></p>
                        <p className='overview-toFormWrap'><span>お問い合わせフォームは</span><button className="link overview-toForm">こちら</button></p>
                    </td>
                </tr>
                <tr>
                    <th>決済</th>
                    <td>
                        現金<br />
                        クレジットカード（JCB、Master、Visa、American Express）<br />
                        電子マネー（交通系、楽天Edy、iD、QUICPay）、QR決済（PayPay、楽天ペイ、LINE Pay、d払い）
                    </td>
                </tr>
                <tr>
                    <th>アクセス</th>
                    <td>
                        <p>{shopOverview[selectedShop].access}</p>
                        <a href={shopOverview[selectedShop].link} target="_blank" rel="noreferrer noopener" className='link'><span>マップで開く</span></a>
                    </td>
                </tr>
            </table>
            <Access selectedShop={selectedShop} shopOverview={shopOverview} />
        </div >
    )
}

export default Overview
```
`useState()`フックで、現在選択されている店舗を管理する関数`selectedShop`を作成します。

初期値は`'shinjuku'`にして、デフォルトでは新宿本店の店舗概要が表示されるようにしておきます。

`setSelectedSjop`で表示される項目を切り替える関数、`handleSelectShop`を定義し、`<button onClick={() => handleSelectShop('店舗名')} />`で、`button`クリックで`handleSelectShop`が発火するようにしています。

![tabChange.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1902958/fc2fcc4b-d12a-3572-befc-47c9eac662f2.gif)

## GoogleマップAPI
マップエリアはiframe埋め込みではなく、[Google Maps PlatformのYouTube動画](https://youtu.be/PfZ4oLftItk?si=ac_SrOQu1IZPqKlY)を参考にAPIを使って実装してみました。

先ほど制作した`shopOverview.js`に、各店舗のmapへのリンクと位置情報を追加します。
```diff_jsx:shopOverview.js
const shopOverview = {
    'shinjuku': {
        // 省略
+        link: 'https://maps.app.goo.gl/4fPZGNwibgr3khuw7',
+        CENTER: {
+            lat: 35.6924892,
+            lng: 139.6987123,
        },
    },
    'ikebukuro': {
        // 省略
+        link: 'https://maps.app.goo.gl/773jf7TkNVwcduk99',
+        CENTER: {
+            lat: 35.7295071,
+            lng: 139.7083252,
        },
    },
    'kinshicho': {
        // 省略
+        link: 'https://maps.app.goo.gl/d3PiNts1DEH3gDKDA',
+       CENTER: {
+            lat: 35.6966451,
+            lng: 139.8119958,
        },
    }
}
```
```jsx:Access.jsx
import { APIProvider, Map, AdvancedMarker, Pin, InfoWindow } from '@vis.gl/react-google-maps';
import { useMediaQuery } from 'react-responsive';

const Access = (props) => {
    const position = props.shopOverview[props.selectedShop].CENTER;
    const zoom = props.shopOverview[props.selectedShop].ZOOM;
    const desktopMapPostion = { lat: 35.7142509, lng: 139.7611782 }
    const shopMessages = {
        shinjuku: '新宿本店',
        ikebukuro: '池袋店',
        kinshicho: '錦糸町店',
    }

    const desktopPositions = [
        props.shopOverview['shinjuku'].CENTER,
        props.shopOverview['ikebukuro'].CENTER,
        props.shopOverview['kinshicho'].CENTER,
    ]
    return (
        <div className='map' style={{ height: '400px', width: '100vw' }}>
            <APIProvider apiKey={'xxxxxxxxxxxxxxxx'}>
                <Map center={position} zoom={16} mapId='xxxxxxxxxxxxxxxx'>
                    <AdvancedMarker position={position} />
                </Map>
            </APIProvider>
        </div>
    )
}

export default Access
```
![map.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1902958/efe3d27a-a9d6-d77e-bff9-eb41da2baab7.jpeg)

一応表示はできましたが、何故かズームやスクロールが出来なくなっています。。
いつか直そうと思います。


 記事が長くなってきたので、ここら辺で一旦終わろうと思います。
 次回はPC画面対応などのコード微調整編です。

 次の記事↓

https://qiita.com/yamashee/items/151d74718c9421a70136