20210425のReactに関する記事は8件です。

create-react-appで環境構築をしてみよう

はじめに JavaScriptのライブラリであるReactでアプリを作成してみたくなったので環境構築までの過程を備忘録としてまとめたいと思います。 create-react-appはReactで簡単にアプリを作成するためのツールです。本来ならbabelやwebpackなどの設定も含めて自分で構築しなければならないのですが1行のコマンドでそれができてしまうのがcreate-react-appです。 create-react-appに必要なツール node バージョン8.10以上 npm バージョン5.6以上 また、上記の2つをインストールするために更にhomebrewとnodebrewをインストールしていきます。 1. homebrewのインストール nodebrewを使うために最初にhomebrewをインストールします。 homebrewとはmacOS用のパッケージマネージャーです。様々なファイルの依存関係の問題を解決するためにパッケージとしてまとめて提供してくれています。 homebrew公式HPからインストール用のコマンドをターミナルにコピペします。 ターミナル $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" // インストールに数分かかります // インストールできたか確認(バージョンが表示されれば成功です) % brew -v Homebrew 3.0.10 Homebrew/homebrew-core (git revision 772716397f; last commit 2021-03-30) 2. nodebrewをインストール homebrewを使ってのインストールはbrew install パッケージ名と記述します。 ターミナル $ brew install nodebrew // インストールできたか確認(バージョンが表示されれば成功です) $ nodebrew -v nodebrew 1.1.0 Usage: nodebrew help Show this message nodebrew install <version> Download and install <version> (from binary) nodebrew compile <version> Download and install <version> (from source) nodebrew install-binary <version> Alias of `install` (For backward compatibility) nodebrew uninstall <version> Uninstall <version> nodebrew use <version> Use <version> nodebrew list List installed versions nodebrew ls Alias for `list` nodebrew ls-remote List remote versions nodebrew ls-all List remote and installed versions nodebrew alias <key> <value> Set alias nodebrew unalias <key> Remove alias nodebrew clean <version> | all Remove source file nodebrew selfupdate Update nodebrew nodebrew migrate-package <version> Install global NPM packages contained in <version> to current version nodebrew exec <version> -- <command> Execute <command> using specified <version> Example: # install nodebrew install v8.9.4 # use a specific version number nodebrew use v8.9.4 ここまででhomebrewとnodebrewを無事インストールできました。 次にnodeとnpmをインストールしていきます。 3. nodeをインストール nodebrew ls-remoteでインストールできるnodeのバージョンを確認します。 ターミナル $ nodebrew ls-remote // 実行結果 v0.0.1 v0.0.2 v0.0.3 v0.0.4 v0.0.5 v0.0.6 v0.1.0 v0.1.1 v0.1.2 v0.1.3 v0.1.4 v0.1.5 v0.1.6 v0.1.7 v0.1.8 v0.1.9 v0.1.10 v0.1.11 v0.1.12 v0.1.13 v0.1.14 v0.1.15 v0.1.16 v0.1.17 v0.1.18 v0.1.19 v0.1.20 v0.1.21 v0.1.22 v0.1.23 v0.1.24 v0.1.25 v0.1.26 v0.1.27 v0.1.28 v0.1.29 v0.1.30 v0.1.31 v0.1.32 v0.1.33 v0.1.90 v0.1.91 v0.1.92 v0.1.93 v0.1.94 v0.1.95 v0.1.96 v0.1.97 v0.1.98 v0.1.99 v0.1.100 v0.1.101 v0.1.102 v0.1.103 v0.1.104 : : : 下にいくほど最新のバージョンになります。 要件的には8.10以上であれば問題ないですが、nodebrew install stableと打てば安定的なバージョンを自動でインストールできます。 しかし、以下のようなエラーが発生することがあります。 ターミナル % nodebrew install stable Fetching: https://nodejs.org/dist/v16.0.0/node-v16.0.0-darwin-x64.tar.gz Warning: Failed to create the file Warning: /Users/XXXXXX/.nodebrew/src/v16.0.0/node-v16.0.0-darwin-x64. Warning: tar.gz: No such file or directory これはnodebrewをインストールする用のディレクトリが存在しない場合に起こります。 この場合は以下のコマンドで解決できます。 ターミナル % nodebrew setup // 実行結果 Fetching nodebrew... Installed nodebrew in $HOME/.nodebrew ======================================== Export a path to nodebrew: export PATH=$HOME/.nodebrew/current/bin:$PATH ======================================== これでnodebrewがインストールできるようになります。 ターミナル % nodebrew install stable Fetching: https://nodejs.org/dist/v16.0.0/node-v16.0.0-darwin-x64.tar.gz ##################################################################################################################################################################################################### 100.0% Installed successfully // successfullyと表示されたら成功です。 // インストールされているnodeを確認 $ nodebrew ls % nodebrew ls v16.0.0 current: none 確認してみるとv16.0.0がインストールされていることがわかります。 現在currentがnoneになっているので、インストールしたバージョンを指定して有効化します。 ターミナル $ nodebrew use v16.0.0 // もう一度確認 $ nodebrew ls v16.0.0 current: v16.0.0 currentがインストールしたバージョンになっていたら成功です。 次にnodeを使用できるように以下のコマンドを入力します。 ターミナル $ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zprofile コマンドを入力したら一旦ターミナルを再起動して結果を反映させます。 ターミナルを開いたらバージョンを確認してみましょう。 ターミナル $ node -v v16.0.0 インストールしたバージョンになっていたら成功です。 4. npmのインストール 次にnpmですが実はnodeをインストールした時点で自動的にインストールされています。 以下のコマンドでバージョンが確認でしてみましょう ターミナル % npm -v 7.10.0 バージョンは5.6以上でしたら問題ないです。 長かったですが、これでやっと準備が整いました! それではいよいよcreate-react-appに移ります。 create-react-app まず、いつも自分が使っているディレクトリに移動しましょう。 今回はprojectsとします。 移動できたらnpxコマンドでアプリを作成していきます。 ターミナル projects $ npx create-react-app 任意のフォルダ名 // 結果 : :省略 : Happy hacking! いろいろフォルダやらファイルが生成されますがまずはエディタを開いて中身を確認していきましょう。 フォルダの中身を確認 エディタを開いたらまずは、ターミナルを立ち上げて以下のコマンドを打ちましょう。 ターミナル $ npm run build これで本番環境に公開するためのbuildというフォルダが作成されます。 ディレクトリ構成は以下のようになっていると思います。 React(任意のフォルダ名) |-build |-node_module |-public |-src |-.gitignore |-package.json |-README |-yarn.lock 次はターミナルで以下のコマンドを打ってみましょう。 ターミナル $ npm start するとサーバーが立ち上がりReactの初期画面が描画されます。 画面にある通り、src/App.jsを編集すると編集内容が画面に反映されます。 試しに、App.jsを以下のように編集してみましょう。 App.js import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React and output // and outputを追記 </a> </header> </div> ); } export default App; 保存してブラウザを確認するとLearn React and outputと表示されると思います。 簡単にまとめるとこんな感じです。 src: コンポーネントを作るためのjsファイルの集まり public: htmlファイルやその他の設定ファイル build: 本番環境用のファイル 以上で環境構築は終了です。お疲れさまでした。。。 参考文献 MacにNode.jsをインストール チュートリアル:React の導入
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactとRailsのアプリを一つのEC2にデプロイする。React編

今回は、ReactとRailsを使ってSPAを作成した際に一つのEC2インスタンスにデプロイする方法を忘備録としてまた、同じ事象で悩んでいる方のために残しておきます。 フロント側をAmplifyやnetlifyでデプロイしようと考えていましたが、RailsのAPI側をhttps化しなければリクエストを受け付けてくれなかったので、EC2に両方ぶち込んじゃえという手法になります。 概要 前提として、今回はフロント側について記述するため、RailsAPI側の方法は後日記述します。 EC2のインバウンドルールを編集しましょう。 まずは、EC2のセキュリティグールプからインバウンドを画像のように編集しましょう。 簡単な説明として、API側のポートとして3000番を設定しています。 また、IPV4とIPV6すべてのリクエストに対応するため、::/0と0.0.0.0/0は2つずつ設定しています。 EC2インスタンスにsshで接続し、ディレクトリを作成しましょう。 私の場合は、~/var/wwwというディレクトリを作成しました。 作成したディレクトリにReactアプリをgit cloneしましょう。 先程作成したディレクトリにgit cloneをしましょう。 EC2インスタンスにgit cloneをする場合は、作成したEC2インスタンスのssh公開鍵をGitHubに登録する必要があります。 EC2サーバーのssh鍵のペアを作成し、GitHubにssh鍵を登録しましょう。 [ec2-user@ip-192-32-33-129 ~]$ ssh-keygen -t rsa -b 4096 Generating public/private rsa key pair. Enter file in which to save the key (/home/ec2-user/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/ec2-user/.ssh/id_rsa. Your public key has been saved in /home/ec2-user/.ssh/id_rsa.pub. The key fingerprint is: 3a:8c:1d:d1:a9:22:c7:6e:6b:43:22:31:0f:ca:63:fa ec2-user@ip-172-31-23-189 The key's randomart image is: +--[ RSA 4096]----+ | + | | . . = | | = . o . | | * o . o | |= * S | |.* + . | | * + | | .E+ . | | .o | +-----------------+ ※Enterを押せば画像のような表記になります。 ssh公開鍵の値をGitHubに登録しましょう。 catコマンドで、公開鍵が含まれているファイルid_rsa.pubの中身をターミナル上に表示します。 [ec2-user@ip-192-32-33-129 ~]$ cat ~/.ssh/id_rsa.pub すべて(ssh-rsaから最後の文字まで)コピーします。 ssh-rsa AAAs3NzaC1yc2EAAASADAQABavasAQDLwt...... https://github.com/settings/keys 上記のURLからkeyを追加しましょう。(タイトルはなんもでもOKです。) git cloneが出来たらyarn run buildを実行しましょう。 先程の作成したディレクトリにgit cloneが完了しましたら、cloneしたアプリに移動し、yarn run buildを実行しましょう。 実行すると、アプリ内にbuildディレクトリが作成され、その中にindex.htmlが生成されていればOKです。 ※yarn run buildを実行してもindex.htmlが生成されない場合はメモリ不足の可能性があります。 yarn run buildコマンドはメモリをかなり食うらしく、何度やっても生成されない場合は、一度EC2を停止させて、再起動し、再度実行してみましょう。 Nginxの設定ファイルを作成し編集しましょう。 Nginxの設定ファイルは/etc/nginx/conf.d/***.confに作成しましょう。(ファイル名は何でもOKです) 作成できましたら、下記のように編集しましょう。 /etc/nginx/conf.d/***.conf server { listen 80; server_name IPアドレス; charset utf-8; # ドキュメントルートを指定. root /var/www/アプリ名/build; index index.html; # リクエストされたリソースがなければ、index.htmlを返却. location / { try_files $uri /index.html; } } 上記でフロントエンド側の設定は終了です。 IPアドレスに接続し、アプリが表示されていればOKです。 ※Nginxの設定ファイルを編集した跡は必ずrestartしましょう。 sudo systemctl restart nginx ありがとうございました。 参考文献リスト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js × TypeScript × microCMSでシンプルなコーポレートサイトを作ってみる

概要 友人の依頼でちょっとしたコーポレートサイトを作る機会があったのでメモ。 使ったもの Next.js(フレームワーク) TypeScript(言語) Material UI(デザイン) microCMS(CMS) Vercel(デプロイ) 特にmicroCMSは前々から使ってみたいと思っていたサービスなので良い機会でした。 (※筆者はまだTypeScriptに慣れておらず雰囲気で書いている事も多いため、所々で未熟な部分があるかもしれません。あらかじめご了承ください。) 完成系 https://corporate-site-sample.vercel.app トップページ 事業紹介ページ 会社情報ページ 採用ページ 一覧 詳細 ブログページ 一覧 詳細 お問い合わせページ コーポレートサイトを制作するにあたってどんなページ構成にするかは好みによると思いますが、今回は トップページ 事業紹介ページ 会社情報ページ 採用ページ ブログページ お問い合わせページ といったシンプルな構成にしてみました。これらはどのコーポレートサイトにおいても非常に良く見かける気がします。 実装 前置きはほどほどに、実装していきたいと思います。 Next.jsプロジェクトを作成 まずは「create-next-app」コマンドを使ってNext.jsプロジェクトを作成しましょう。 $ create-next-app corporate-site-sample $ cd corporate-site-sample TypeScriptを導入 今回はTypeScriptで書いていくつもりなので、早めに導入しておきます。 $ touch tsconfig.json $ yarn add --dev typescript @types/react @types/node ビルド $ yarn dev ... We detected TypeScript in your project and created a tsconfig.json file for you. event - compiled successfully 「We detected TypeScript in your project and created a tsconfig.json file for you.」というメッセージが返ってくるとともにルートプロジェクトに「next-env.d.ts」というファイルが作成され、tsconfig.jsonの中身も変わっているので確認してください。 ./tsconfig.json { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "strict": false, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve" }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx" ], "exclude": [ "node_modules" ] } 既存ファイルの拡張子をtsxに変更します。 $ find pages -name "_app.js" -or -name "index.js" | sed 'p;s/.js$/.tsx/' | xargs -n2 mv & \ find pages/api -name "*.js" | sed 'p;s/.js$/.ts/' | xargs -n2 mv ./pages/index.tsx const Home: React.FC = () => { return ( <h1>Hello World!!</h1> ) } export default Home 「.pages/index.tsx」を適当にTypeScriptっぽく書き換えて再度ビルド。http://localhost:3000/ にアクセスして「Hello World!!」と返ってくれば無事TypeScriptの導入に成功です。 各種ディレクトリ・ファイルを準備 これから大量のディレクトリ・ファイルを取り扱う事になるので、あらかじめ準備しておきましょう。一個一個ぽちぽち作っていくのはさすがに辛いので、シェルスクリプトを用意して一気にまとめて作ってしまいます。 $ cat > provisioning.sh << EOS #!/bin/bash mkdir components mkdir components/blog mkdir components/company mkdir components/contact mkdir components/home mkdir components/layouts mkdir components/recruit mkdir components/service mkdir components/utils mkdir data mkdir lib mkdir pages/blog mkdir pages/blog/page mkdir pages/recruit touch components/blog/Post.tsx touch components/blog/Posts.tsx touch components/company/About.tsx touch components/contact/Form.tsx touch components/home/Introductions.tsx touch components/home/Slider.tsx touch components/layouts/Footer.tsx touch components/layouts/Header.tsx touch components/layouts/PageTemplate.tsx touch components/recruit/Jobs.tsx touch components/recruit/Qualifications.tsx touch components/recruit/Slider.tsx touch components/service/Features.tsx touch components/utils/Link.tsx touch components/utils/ScrollUp.tsx touch components/utils/ShareButton.tsx touch components/utils/SocialMedia.tsx touch components/utils/theme.ts touch data/routes.ts touch lib/api.ts touch "pages/blog/page/[number].tsx" touch "pages/blog/[id].tsx" touch "pages/recruit/[job].tsx" touch pages/_document.tsx touch pages/company.tsx touch pages/contact.tsx touch pages/recruit.tsx touch pages/service.tsx touch .env.local mkdir src mv components data lib pages styles src EOS 実行しましょう。 $ sh provisioning.sh 最終的に次のような構成になっていればOKです。 ├── .next ├── node_modules ├── public │   ├── favicon.ico │   └── vercel.svg ├── src │   ├── components │   │   ├── blog │   │   │   ├── Post.tsx │   │   │   └── Posts.tsx │   │   ├── company │   │   │   └── About.tsx │   │   ├── contact │   │   │   └── Form.tsx │   │   ├── home │   │   │   ├── Introductions.tsx │   │   │   └── Slider.tsx │   │   ├── layouts │   │   │   ├── Footer.tsx │   │   │   ├── Header.tsx │   │   │   └── PageTemplate.tsx │   │   ├── recruit │   │   │   ├── Jobs.tsx │   │   │   ├── Qualifications.tsx │   │   │   └── Slider.tsx │   │   ├── service │   │   │   └── Features.tsx │   │   └── utils │   │   ├── Link.tsx │   │   ├── ScrollUp.tsx │   │   ├── ShareButton.tsx │   │   ├── SocialMedia.tsx │   │   └── theme.ts │   ├── data │   │   └── routes.ts │   ├── lib │   │   └── api.ts │   ├── pages │   │   ├── _app.tsx │   │   ├── _document.tsx │   │   ├── api │   │   │   └── hello.ts │   │   ├── blog │   │   │   ├── [id].tsx │   │   │   └── page │   │   │   └── [number].tsx │   │   ├── company.tsx │   │   ├── contact.tsx │   │   ├── index.tsx │   │   ├── recruit │   │   │   └── [job].tsx │   │   ├── recruit.tsx │   │   └── service.tsx │   └── styles │   ├── Home.module.css │   └── globals.css ├── .env.local ├── .gitignores ├── next-env.d.ts ├── package.json ├── provisioning.sh ├── tsconfig.json └── yarn.lock Material UIを導入 デザインを整えるためにMateriau UIを導入していきます。 $ yarn add @material-ui/core @material-ui/icons @material-ui/lab ./src/components/utils/theme.ts import { createMuiTheme } from "@material-ui/core/styles" import { red } from "@material-ui/core/colors" const black = "#212121" const white = "#fafafa" const blue = "#757ce8" const theme = createMuiTheme({ palette: { common: { black: black, white: white }, primary: { main: black }, secondary: { main: white }, info: { main: blue }, error: { main: red.A400 } }, typography: { h1: { fontSize: "3rem", fontWeight: 500, }, h2: { fontSize: "2rem", fontWeight: 500, }, h3: { fontSize: "1.25rem", fontWeight: 500, }, h4: { fontSize: "1rem", fontWeight: 500, } } }) export default theme ./src/pages/_document.tsx import React from "react" import Document, { Head, Html, Main, NextScript } from "next/document" import { ServerStyleSheets } from "@material-ui/core/styles" import theme from "../components/utils/theme" export default class MyDocument extends Document { render() { return ( <Html lang="ja-JP"> <Head> {/* PWA primary color */} <meta name="theme-color" content={theme.palette.primary.main} /> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ) } } MyDocument.getInitialProps = async (ctx) => { const sheets = new ServerStyleSheets() const originalRenderPage = ctx.renderPage ctx.renderPage = () => originalRenderPage({ enhanceApp: (App) => (props) => sheets.collect(<App {...props} />) }) const initialProps = await Document.getInitialProps(ctx) return { ...initialProps, styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()] } } ./src/pages/_app.tsx import React, { useEffect } from "react" import Head from "next/head" import { AppProps } from "next/app" import { ThemeProvider } from "@material-ui/core/styles" import CssBaseline from "@material-ui/core/CssBaseline" import theme from "../components/utils/theme" import "../styles/globals.css" export default function MyApp({ Component, pageProps }: AppProps) { useEffect(() => { // Remove the server-side injected CSS. const jssStyles = document.querySelector("#jss-server-side") if (jssStyles) { jssStyles.parentElement.removeChild(jssStyles) } }, []) return ( <React.Fragment> <Head> <title>Corporate Site Sample</title> <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" /> </Head> <ThemeProvider theme={theme}> {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} <CssBaseline /> <Component {...pageProps} /> </ThemeProvider> </React.Fragment> ) } ./src/pages/index.tsx import Button from "@material-ui/core/Button" const Home: React.FC = () => { return ( <> <h1>Hello World!!</h1> <Button variant="contained">Default</Button> </> ) } export default Home ちゃんとそれっぽいボタンが表示されていればMaterial UIの導入に成功です。 その他ライブラリをインストール 他にも後に使う予定のライブラリがあるので、まとめてインストールしてしまいましょう。 $ yarn add lightbox-react moment react-items-carousel react-material-ui-carousel react-scroll-to-top react-share lightbox-react 画像クリックで拡大機能が作れる moment 日付データの操作を簡単にしてくれる react-items-carousel カルーセル (スライダー)が作れる react-material-ui-carousel 同上 react-scroll-to-top -スムーススクロールが作れる react-share SNSシェアボタンが作れる ルーティングを設定 ./src/data/routes.ts export const routes = [ { name: "Service", link: "/service" }, { name: "Company", link: "/company" }, { name: "Recruit", link: "/recruit" }, { name: "Blog", link: "/blog/page/1" }, { name: "Contact", link: "/contact" } ] ./src/components/utils/Link.tsx import * as React from "react" import clsx from "clsx" import { useRouter } from "next/router" import NextLink, { LinkProps as NextLinkProps } from "next/link" import MuiLink, { LinkProps as MuiLinkProps } from "@material-ui/core/Link" interface NextLinkComposedProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href">, Omit<NextLinkProps, "href" | "as"> { to: NextLinkProps["href"] linkAs?: NextLinkProps["as"] href?: NextLinkProps["href"] } export const NextLinkComposed = React.forwardRef<HTMLAnchorElement, NextLinkComposedProps>( function NextLinkComposed(props, ref) { const { to, linkAs, href, replace, scroll, passHref, shallow, prefetch, locale, ...other } = props return ( <NextLink href={to} prefetch={prefetch} as={linkAs} replace={replace} scroll={scroll} shallow={shallow} passHref={passHref} locale={locale} > <a ref={ref} {...other} /> </NextLink> ) }, ) export type LinkProps = { activeClassName?: string as?: NextLinkProps["as"] href: NextLinkProps["href"] noLinkStyle?: boolean } & Omit<NextLinkComposedProps, "to" | "linkAs" | "href"> & Omit<MuiLinkProps, "href"> const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(function Link(props, ref) { const { activeClassName = "active", as: linkAs, className: classNameProps, href, noLinkStyle, role, // Link don"t have roles. ...other } = props const router = useRouter() const pathname = typeof href === "string" ? href : href.pathname const className = clsx(classNameProps, { [activeClassName]: router.pathname === pathname && activeClassName, }) const isExternal = typeof href === "string" && (href.indexOf("http") === 0 || href.indexOf("mailto:") === 0) if (isExternal) { if (noLinkStyle) { return <a className={className} href={href as string} ref={ref as any} {...other} /> } return <MuiLink className={className} href={href as string} ref={ref} {...other} /> } if (noLinkStyle) { return <NextLinkComposed className={className} ref={ref as any} to={href} {...other} /> } return ( <MuiLink component={NextLinkComposed} linkAs={linkAs} className={className} ref={ref} to={href} {...other} style={{ textDecoration: "none" }} /> ) }) export default Link 共通レイアウトを作成 ヘッダーやフッターは全てのページで共通にしたいので、テンプレートとしてまとめておきます。 ./src/components/Header.tsx import React, { useState } from "react" import { useRouter } from "next/router" import { makeStyles, useTheme, Theme } from "@material-ui/core/styles" import useMediaQuery from "@material-ui/core/useMediaQuery" import { Grid, AppBar, Toolbar, Typography, List, ListItem, ListItemText, SwipeableDrawer, IconButton, } from "@material-ui/core" import useScrollTrigger from "@material-ui/core/useScrollTrigger" import MenuIcon from "@material-ui/icons/Menu" import Link from "../utils/Link" import { routes } from "../../data/routes" interface ElevationScrollProps { children: React.ReactElement } function ElevationScroll(props: ElevationScrollProps) { const { children } = props const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 }) return React.cloneElement(children, { elevation: trigger ? 4 : 0 }) } const useStyles = makeStyles((theme: Theme) => ({ toolbarMargin: { ...theme.mixins.toolbar, [theme.breakpoints.down("md")]: { }, [theme.breakpoints.down("xs")]: { } }, drawerIconContainer: { marginLeft: "auto", padding: 0, "&:hover": { backgroundColor: "transparent" } }, drawerIcon: { height: "50px", width: "50px", color: "inherit", [theme.breakpoints.down("xs")]: { height: "40px", width: "40px" } }, drawer: { backgroundColor: theme.palette.secondary.main, padding: "0 6em" } })) const Header = () => { const classes = useStyles() const theme = useTheme() const iOS = process.browser && /iPad|iPhone|iPod/.test(navigator.userAgent) const matches = useMediaQuery(theme.breakpoints.down("sm")) const [openDrawer, setOpenDrawer] = useState(false) const router = useRouter() const path = routes const tabs = ( <> <Grid container justify="flex-end" spacing={4}> {path.map(({ name, link }) => ( <Grid item key={link}> <Link href={link}> <Typography style={{ color: "inherit", fontWeight: router.pathname.match(link) ? "bold" : "normal", borderBottom: router.pathname.match(link) && "1px solid #757ce8", }} > {name} </Typography> </Link> </Grid> ))} </Grid> </> ) const drawer = ( <> <SwipeableDrawer disableBackdropTransition={!iOS} disableDiscovery={iOS} open={openDrawer} onClose={() => setOpenDrawer(false)} onOpen={() => setOpenDrawer(true)} classes={{ paper: classes.drawer }} anchor="right" > <div className={classes.toolbarMargin} /> <List disablePadding> {path.map(({ name, link }) => ( <ListItem key={link} divider button onClick={() => { setOpenDrawer(false) }} > <ListItemText disableTypography> <Link href={link}> <Typography style={{ color: router.pathname === link ? "primary" : "rgb(107 107 107)", fontWeight: router.pathname === link ? "bold" : "normal" }} > {name} </Typography> </Link> </ListItemText> </ListItem> ))} </List> </SwipeableDrawer> <IconButton onClick={() => setOpenDrawer(!openDrawer)} disableRipple className={classes.drawerIconContainer} > <MenuIcon className={classes.drawerIcon} /> </IconButton> </> ) return ( <> <ElevationScroll> <AppBar color="inherit"> <Toolbar disableGutters style={{ maxWidth: "1280px", margin: "0 auto", width: "100%", padding: matches ? "0 16px" : "24px" }} > <Link href="/"> <Typography style={{ color: "inherit", fontWeight: "bold", fontSize: "1.75em", position: "relative", zIndex: 100 }} > Sample </Typography> </Link> {matches ? drawer : tabs} </Toolbar> </AppBar> </ElevationScroll> <div className={classes.toolbarMargin} /> </> ) } export default Header ./src/components/Footer.tsx import { useRouter } from "next/router" import { makeStyles, Theme } from "@material-ui/core/styles" import { Container, Grid, Typography } from "@material-ui/core" import { routes } from "../../data/routes" import Link from "../utils/Link" import SocialMedia from "../utils/SocialMedia" const useStyles = makeStyles((theme: Theme) => ({ footer: { backgroundColor: theme.palette.primary.main, width: `100%`, position: "relative", overflow: "hidden", marginTop: "6em", padding: "2em 0 " }, link: { fontSize: "1.25em", color: "#fff" }, contact: { color: "#fff", fontSize: "1.5em", marginTop: "20px" }, copylight: { marginTop: "15px", color: "#fff", fontSize: "1em" } })) const Footer = () => { const classes = useStyles() const path = routes const router = useRouter() return ( <div className={classes.footer}> <Container maxWidth="lg"> <Grid container spacing={3} justify="center"> {path.map(({ name, link }) => ( <Grid item key={link}> <Link href={link}> <Typography className={classes.link} style={{ fontWeight: router.pathname.match(link) ? "bold" : "normal", borderBottom: router.pathname.match(link) && "1px solid #757ce8" }} > {name} </Typography> </Link> </Grid> ))} </Grid> <Grid container direction="column" style={{ margin: "1.5em 0" }}> <SocialMedia /> </Grid> <Grid item container justify="center" > <Typography className={classes.copylight}> &copy;{new Date().getFullYear()} Sample </Typography> </Grid> </Container> </div> ) } export default Footer ./src/components/utils/ScrollUp.tsx import ScrollToTop from "react-scroll-to-top" import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp" const ScrollUp: React.FC = () => { return( <> <ScrollToTop smooth component={<KeyboardArrowUpIcon />} style={{ borderRadius: "50%" }} /> </> ) } export default ScrollUp ./src/components/SocialMedia.tsx import { makeStyles, Theme } from "@material-ui/core/styles" import { Grid } from "@material-ui/core" import MailIcon from "@material-ui/icons/Mail" import TwitterIcon from "@material-ui/icons/Twitter" import InstagramIcon from "@material-ui/icons/Instagram" import FacebookIcon from "@material-ui/icons/Facebook" const useStyles = makeStyles((theme: Theme) => ({ snsIcon: { width: "30px", height: "30px", [theme.breakpoints.down("xs")]: { width: "25px", height: "25px", } } })) interface SocialMediaProps { color?: string } const SocialMedia = ({ color }: SocialMediaProps) => { const classes = useStyles() return ( <Grid item container spacing={2} justify="center"> <Grid item component={"a"} target="_blank" rel="noreferrer noopener" href="/contact" > <MailIcon className={classes.snsIcon} color={color ? "primary" : "secondary"} /> </Grid> <Grid item component={"a"} target="_blank" rel="noreferrer noopener" href="" > <TwitterIcon className={classes.snsIcon} color={color ? "primary" : "secondary"} /> </Grid> <Grid item component={"a"} target="_blank" rel="noreferrer noopener" href="" > <InstagramIcon className={classes.snsIcon} color={color ? "primary" : "secondary"} /> </Grid> <Grid item component={"a"} target="_blank" rel="noreferrer noopener" href="" > <FacebookIcon className={classes.snsIcon} color={color ? "primary" : "secondary"} /> </Grid> </Grid> ) } export default SocialMedia ./src/components/PageTemplate.tsx import Head from "next/head" import React from "react" import Header from "./Header" import Footer from "./Footer" import ScrollUp from "../utils/ScrollUp" interface PageTemplateProps { children: React.ReactElement title: string } const PageTemplate = ({ children, title }: PageTemplateProps) => { return ( <> <Head> <title>{title ? title : "Corporate Site Sample"}</title> </Head> <header> <Header /> </header> <main> {children} </main> <ScrollUp /> <footer> <Footer /> </footer> <style jsx global> {` html, body { background: #F5F5F5; overflow-x: hidden; padding: 0 !important; } #__next { min-height: 100vh; display: flex; flex-direction: column; justify-content: space-between; } main { flex: 1; } `} </style> </> ) } export default PageTemplate ./src/pages/index.tsx import { ThemeProvider } from "@material-ui/core/styles" import PageTemplate from "../components/layouts/PageTemplate" import theme from "../components/utils/theme" const Home: React.FC = () => { return ( <> <ThemeProvider theme={theme}> <PageTemplate title="Home | Corporate Site Sample"> <h1>Hello World!!</h1> </PageTemplate> </ThemeProvider> </> ) } export default Home こんな感じでヘッダーとフッターが良い感じになっていればOK. トップページを作成 ./src/components/home/Slider.tsx import React from "react" import { makeStyles } from "@material-ui/core/styles" import { Button, Paper, Typography } from "@material-ui/core" import Carousel from "react-material-ui-carousel" const useStyles = makeStyles(() => ({ slider: { width: "100%" }, media: { position: "relative", height: "300px", overflow: "hidden", padding: "20px", color: "white" }, checkButton: { marginTop: "40px", color: "#fff", fontSize: "25px", border: "3px solid white", textTransform: "capitalize" } })) interface ItemProps { name: string description: string color: string } const Item = ({ name, description, color }: ItemProps) => { const classes = useStyles() return ( <Paper className={classes.media} style={{ backgroundColor: color }} elevation={10} square > <h2>{name}</h2> <p>{description}</p> <Button className={classes.checkButton}> Check it out! </Button> </Paper> ) } const Slider = ({ items }) => { const classes = useStyles() return ( <Carousel className={classes.slider} autoPlay={items.length > 1 ? true : false} animation="fade" navButtonsAlwaysInvisible={items.length == 1 ? true : false} indicators={false} timeout={300} > { items.map((item, index) => ( <Item key={index} name={item.name} description={item.description} color={item.color} /> )) } </Carousel> ) } export default Slider ./src/components/home/Introductions.tsx import { makeStyles, Theme } from "@material-ui/core/styles" import { Typography, Box, Button } from "@material-ui/core" const useStyles = makeStyles((theme: Theme) => ({ linkButton: { marginTop: theme.spacing(2), textTransform: "none", border: "transparent 1px solid", borderRadius: 50, backgroundColor: "#4F9DF7", color: "#fff", "&:hover": { backgroundColor: "#fff", color: "#4F9DF7" } } })) interface IntroductionsProps { index: number title: string description: string action: string href: string } const Introductions = ({ index, title, description, action, href }: IntroductionsProps) => { const classes = useStyles() return ( <> <Typography variant="h1" gutterBottom align={index % 2 == 0 ? "left" : "right"}> {title} </Typography> <Typography variant="body1" align={index % 2 == 0 ? "left" : "right"} paragraph> {description} </Typography> <Box textAlign={index % 2 == 0 ? "left" : "right"}> <Button variant="outlined" color="primary" className={classes.linkButton} href={href}> {action} </Button> </Box> </> ) } export default Introductions ./src/pages/index.tsx import { makeStyles, ThemeProvider } from "@material-ui/core/styles" import { Container, Grid} from "@material-ui/core" import Slider from "../components/home/Slider" import Introductions from "../components/home/Introductions" import PageTemplate from "../components/layouts/PageTemplate" import theme from "../components/utils/theme" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem" } })) interface Item { name: string description: string color: string } interface Introduction { title: string description: string action: string href: string } const Home: React.FC = () => { const classes = useStyles() const items: Item[] = [ { name: "Slide1", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", color: "#64ACC8" }, { name: "Slide2", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", color: "#7D85B1" }, { name: "Slide3", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", color: "#CE7E78" } ] const introductions: Introduction[] = [ { title: "Service", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", action: "About Service >", href: "/service" }, { title: "Company", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", action: "About Company >", href: "/company" }, { title: "Recruit", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", action: "About Recruit >", href: "/recruit" } ] return ( <> <ThemeProvider theme={theme}> <PageTemplate title="Home | Corporate Site Sample"> <> <Slider items={items} /> { introductions.map((introduction, index) => ( <Container key={index} maxWidth="lg" className={classes.container}> <Grid container justify={index % 2 == 0 ? "flex-start" : "flex-end"}> <Grid item lg={6} md={6}> <Introductions index={index} title={introduction.title} description={introduction.description} action={introduction.action} href={introduction.href} /> </Grid> </Grid> </Container> )) } </> </PageTemplate> </ThemeProvider> </> ) } export default Home こんな感じになっていればOKです。 Serviceページを作成 ./src/components/service/Features.tsx import { Typography } from "@material-ui/core" interface FeaturesProps { title: string description: string } const Features = ({ title, description }: FeaturesProps) => { return ( <> <div style={{marginTop: "3rem"}}> <Typography variant="h2" align="left" gutterBottom> {title} </Typography> <Typography variant="body1"> {description} </Typography> </div> </> ) } export default Features ./src/pages/service.tsx import { makeStyles, ThemeProvider } from "@material-ui/core/styles" import { Container, Grid, Typography} from "@material-ui/core" import Features from "../components/service/Features" import PageTemplate from "../components/layouts/PageTemplate" import theme from "../components/utils/theme" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem" } })) interface Feature { title: string description: string } const Service: React.FC = () => { const classes = useStyles() const features: Feature[] = [ { title: "Feature1", description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." }, { title: "Feature2", description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." }, { title: "Feature3", description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." } ] return ( <> <ThemeProvider theme={theme}> <PageTemplate title="Service | Corporate Site Sample"> <> <Container maxWidth="lg"className={classes.container}> <Grid container justify="center"> <Grid item> <Typography variant="h1" gutterBottom> Service </Typography> </Grid> </Grid> </Container> <Container maxWidth="lg" className={classes.container}> { features.map((feature, index) => ( <Features key={index} title={feature.title} description={feature.description} /> )) } </Container> </> </PageTemplate> </ThemeProvider> </> ) } export default Service こんな感じになっていればOKです。 Company(企業情報)ページを作成。 ./src/components/compamy/About.tsx import { makeStyles, Theme } from "@material-ui/core/styles" import Table from "@material-ui/core/Table" import TableBody from "@material-ui/core/TableBody" import TableCell from "@material-ui/core/TableCell" import TableContainer from "@material-ui/core/TableContainer" import TableRow from "@material-ui/core/TableRow" import Paper from "@material-ui/core/Paper" const useStyles = makeStyles((theme: Theme) => ({ table: { minWidth: 650 } })) const createData = (key: string, value: string) => { return { key, value } } interface AboutProps { name: string founded: string capital: string ceo: string address: string service: string mail: string } const About = ({ name, founded, capital, ceo, address, service, mail }: AboutProps) => { const classes = useStyles() const rows = [ createData("Name", name), createData("Founded", founded), createData("Capital", capital), createData("CEO", ceo), createData("Address", address), createData("Service", service), createData("Mail", mail), ] return ( <TableContainer component={Paper}> <Table className={classes.table}> <TableBody> {rows.map((row) => ( <TableRow key={row.key}> <TableCell component="th" scope="row" style={{ fontWeight: "bold"}}> {row.key} </TableCell> <TableCell>{row.value}</TableCell> </TableRow> ))} </TableBody> </Table> </TableContainer> ) } export default About ./src/pages/company.tsx import { makeStyles, ThemeProvider } from "@material-ui/core/styles" import { Container, Grid, Typography} from "@material-ui/core" import About from "../components/company/About" import PageTemplate from "../components/layouts/PageTemplate" import theme from "../components/utils/theme" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem" } })) interface About { name: string founded: string capital: string ceo: string address: string service: string mail: string } const Company: React.FC = () => { const classes = useStyles() const about: About = { name: "ABC Company", founded: "2021/01/01", capital: "20,000,000 JPY", ceo: "Taro Yamada", address: "Tokyo Skytree 1 Chome-1-2 Oshiage, Sumida, Tokyo", service: "Engineering", mail: "abc@example.com" } return ( <> <ThemeProvider theme={theme}> <PageTemplate title="Company | Corporate Site Sample"> <> <Container maxWidth="lg" className={classes.container}> <Grid container justify="center"> <Grid item> <Typography variant="h1" gutterBottom> Company </Typography> </Grid> </Grid> </Container> <Container maxWidth="lg" className={classes.container}> <About name={about.name} founded={about.founded} capital={about.capital} ceo={about.ceo} address={about.address} service={about.service} mail={about.mail} /> </Container> </> </PageTemplate> </ThemeProvider> </> ) } export default Company こんな感じになってればOKです。 Recruit(採用)ページを作成 ./src/components/recruit/jobs/.tsx import React from "react" import { makeStyles, Theme } from "@material-ui/core/styles" import { Grid, Paper } from "@material-ui/core" import Link from "../utils/Link" const useStyles = makeStyles((theme: Theme) => ({ paper: { padding: "1rem", height: "100%", textAlign: "center", color: theme.palette.text.secondary }, })) interface JobProps { name: string } const Job = ({ name }: JobProps) => { const classes = useStyles() return ( <Grid item xs={6}> <Link href="/recruit/[job]" as={`/recruit/${name.toLowerCase()}`}> <Paper className={classes.paper}>{name}</Paper> </Link> </Grid> ) } const Jobs = ({ jobs }) => { return ( <> { jobs.map((job, index) => ( <Job key={index} name={job.name} /> ))} </> ) } export default Jobs ./src/components/Qualifications.tsx import { makeStyles } from "@material-ui/core/styles" import { Grid} from "@material-ui/core" import Card from "@material-ui/core/Card" import CardHeader from "@material-ui/core/CardHeader" import List from "@material-ui/core/List" import ListItem from "@material-ui/core/ListItem" import ListItemText from "@material-ui/core/ListItemText" const useStyles = makeStyles(() => ({ card: { height: "100%", marginBottom: "0.5rem", transition: "all 0.3s", "&:hover": { boxShadow: "1px 0px 20px -1px rgba(0,0,0,0.2), 0px 0px 20px 5px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)", transform: "translateY(-3px)" } }, cardHeader: { padding: "1rem 1rem 0.5rem" } })) interface QualificationsProps { requiredSkills: string[] welcomeSkills: string[] idealImages: string[] } const Qualifications = ({ requiredSkills, welcomeSkills, idealImages}: QualificationsProps) => { const classes = useStyles() return ( <> <Grid item xs={12} sm={6} md={4}> <Card className={classes.card}> <CardHeader title="Required Skills" className={classes.cardHeader} /> <List> { requiredSkills.map((requiredSkill, index) => ( <ListItem key={index}> <ListItemText secondary={`・${requiredSkill}`} /> </ListItem> ))} </List> </Card> </Grid> <Grid item xs={12} sm={6} md={4}> <Card className={classes.card}> <CardHeader title="Welcome Skills" className={classes.cardHeader} /> <List> { welcomeSkills.map((welcomeSkill, index) => ( <ListItem key={index}> <ListItemText secondary={`・${welcomeSkill}`} /> </ListItem> ))} </List> </Card> </Grid> <Grid item xs={12} sm={6} md={4}> <Card className={classes.card}> <CardHeader title="Ideal Images" className={classes.cardHeader} /> <List> { idealImages.map((idealImage, index) => ( <ListItem key={index}> <ListItemText secondary={`・${idealImage}`} /> </ListItem> ))} </List> </Card> </Grid> </> ) } export default Qualifications ./src/components/Slider.tsx import React, { useState } from "react" import ItemsCarousel from "react-items-carousel" import Lightbox from "lightbox-react" import "lightbox-react/style.css" import ArrowBackIosIcon from "@material-ui/icons/ArrowBackIos" import ArrowForwardIosIcon from "@material-ui/icons/ArrowForwardIos" const Slider = ({ images }) => { const [activeItemIndex, setActiveItemIndex] = useState(0) const chevronWidth = 40 const [photoIndex, setIndex] = useState(0) const [isOpen, setisOpen] = useState(false) return ( <div style={{ padding: `0 ${chevronWidth}px` }}> <ItemsCarousel requestToChangeActive={setActiveItemIndex} activeItemIndex={activeItemIndex} numberOfCards={2} infiniteLoop gutter={20} leftChevron={<ArrowBackIosIcon />} rightChevron={<ArrowForwardIosIcon />} outsideChevron chevronWidth={chevronWidth} > {images.map((img, index) => { return <img key={index} src={img} style={{ "width": "100%", "height": "100%" }} onClick={() => { setisOpen(true), setIndex(index) }} /> })} {isOpen && ( <Lightbox mainSrc={images[photoIndex]} nextSrc={images[(photoIndex + 1) % images.length]} prevSrc={images[(photoIndex + images.length - 1) % images.length]} onCloseRequest={() => setisOpen(false)} onMovePrevRequest={() => setIndex((photoIndex + images.length - 1) % images.length) } onMoveNextRequest={() => setIndex((photoIndex + 1) % images.length)} clickOutsideToClose={true} enableZoom={false} imagePadding={100} /> )} </ItemsCarousel> </div> ) } export default Slider ./src/recruit/[job].tsx import { GetStaticPaths, GetStaticProps } from "next" import { makeStyles, ThemeProvider } from "@material-ui/core/styles" import Qualifications from "../../components/recruit/Qualifications" import { Container, Grid, Typography} from "@material-ui/core" import PageTemplate from "../../components/layouts/PageTemplate" import theme from "../../components/utils/theme" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem" }, gridItem: { maxWidth: "1260px" } })) const jobs: string[] = [ "planner", "engineer", "designer", "marketer" ] interface JobDetail { image: string name: string description: string requiredSkills: string[] welcomeSkills: string[] idealImages: string[] } const jobDetails: JobDetail[] = [ { image: "https://www.pakutaso.com/shared/img/thumb/MAX75_yubisasu20141025120158_TP_V.jpg", name: "Plannner", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", requiredSkills: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit." ], welcomeSkills: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit." ], idealImages: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit." ] }, { image: "https://www.pakutaso.com/shared/img/thumb/PAK85_MBAdesagyou20140312_TP_V.jpg", name: "Engineer", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", requiredSkills: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit." ], welcomeSkills: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit." ], idealImages: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit." ] }, { image: "https://www.pakutaso.com/shared/img/thumb/N112_nekutaiwonaosudansei_TP_V.jpg", name: "Designer", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", requiredSkills: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit." ], welcomeSkills: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit." ], idealImages: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit." ] }, { image: "https://www.pakutaso.com/shared/img/thumb/OOK82_gurafuwoyubisasu20131223_TP_V.jpg", name: "Marketer", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", requiredSkills: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit." ], welcomeSkills: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit." ], idealImages: [ "Lorem ipsum dolor sit amet.", "Ut enim ad minim veniam.", "Duis aute irure dolor in reprehenderit." ] } ] export const getStaticPaths: GetStaticPaths = async () => { const paths = jobs.map((job) => `/recruit/${job}`) return { paths, fallback: false } } export const getStaticProps: GetStaticProps = async ({ params }) => { const jobDetail = jobDetails[jobs.indexOf(String(params.job))] return { props: { jobDetail }, revalidate: 1 } } const RecruitJob = ({ jobDetail }) => { const classes = useStyles() return ( <ThemeProvider theme={theme}> <PageTemplate title="Recruit | Corporate Site Sample"> <> <Container maxWidth="lg" className={classes.container}> <Grid container justify="center"> <Grid item> <Typography variant="h1" gutterBottom> {jobDetail.name} </Typography> </Grid> </Grid> </Container> <Container maxWidth="lg" className={classes.container}> <Grid container justify="center"> <Grid item className={classes.gridItem}> <img src={jobDetail.image} style={{ height: "auto", maxWidth: "100%" }} /> </Grid> </Grid> </Container> <Container maxWidth="lg" className={classes.container}> <Grid container justify="center"> <Grid item className={classes.gridItem}> <Typography variant="h2" gutterBottom> Description </Typography> <Typography> {jobDetail.description} </Typography> </Grid> </Grid> </Container> <Container maxWidth="lg"className={classes.container}> <Grid container spacing={2}> <Qualifications requiredSkills={jobDetail.requiredSkills} welcomeSkills={jobDetail.welcomeSkills} idealImages={jobDetail.idealImages} /> </Grid> </Container> </> </PageTemplate> </ThemeProvider> ) } export default RecruitJob ./src/recruit.tsx import { makeStyles, ThemeProvider } from "@material-ui/core/styles" import { Container, Grid, Typography} from "@material-ui/core" import Jobs from "../components/recruit/Jobs" import Slider from "../components/recruit/Slider" import PageTemplate from "../components/layouts/PageTemplate" import theme from "../components/utils/theme" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem", padding: "0 1rem" } })) interface Job { name: string } const Recruit: React.FC = () => { const classes = useStyles() const jobs: Job[] = [ { name: "Planner" }, { name: "Engineer" }, { name: "Designer" }, { name: "Marketer" } ] const images: string[] = [ "https://www.pakutaso.com/shared/img/thumb/OOK82_gurafuwoyubisasu20131223_TP_V.jpg", "https://www.pakutaso.com/shared/img/thumb/PAK85_MBAdesagyou20140312_TP_V.jpg", "https://www.pakutaso.com/shared/img/thumb/N112_nekutaiwonaosudansei_TP_V.jpg", "https://www.pakutaso.com/shared/img/thumb/MAX75_yubisasu20141025120158_TP_V.jpg" ] return ( <> <ThemeProvider theme={theme}> <PageTemplate title="Recruit | Corporate Site Sample"> <> <Container maxWidth="lg" className={classes.container}> <Grid container justify="center"> <Grid item> <Typography variant="h1" gutterBottom> Recruit </Typography> </Grid> </Grid> </Container> <Container maxWidth="lg" className={classes.container}> <Grid container spacing={2} justify="center"> <Grid container item xs={12} spacing={2}> <Jobs jobs={jobs} /> </Grid> </Grid> </Container> <Container maxWidth="lg" className={classes.container}> <Slider images={images} /> </Container> <Container maxWidth="lg" className={classes.container}> <Typography align="center"> Please feel free to contact us. </Typography> </Container> </> </PageTemplate> </ThemeProvider> </> ) } export default Recruit 一覧ページ、詳細ページともにこんな感じになっていればOKです。 ブログページを作成 今回、ブログに関しては「micro CMS」を利用して配信していきます。 まず、以下の公式マニュアルに沿ってサービスの作成まで進めてください。 アカウント登録 ログイン サービスの作成 API名: 任意(わかりやすければ何でもOK) エンドポイント: 任意(今回はblog) 型: リスト形式 title(タイトル) テキストフィールド subTitle(サブタイトル) テキストエリア body(本文) リッチエディタ thumbnail(サムネイル画像) 画像 APIスキーマの作成が終わったら、「コンテンツの追加」から適当に記事を作成して公開します。 公開できたら、右上の「APIプレビュー」をクリック。 するとこんな感じでコンテンツを取得するためのcurlコマンドが表示されるので、ターミナルなどから確認してください。また、APIキーはメモなどに控えておきましょう。 ./env.local NEXT_PUBLIC_MICRO_CMS_SERVICE_ID=<micro CMSのサービスID(ドメインみたいな部分)> NEXT_PUBLIC_MICRO_CMS_API_KEY=<micro CMSのAPIキー> ./src/lib/api.ts interface Post { id: string title: string subTitle: string body: HTMLElement thumbnail: string } const serviceId: string = process.env.NEXT_PUBLIC_MICRO_CMS_SERVICE_ID const baseUrl: string = `https://${serviceId}.microcms.io/api/v1` const apiKey: string = process.env.NEXT_PUBLIC_MICRO_CMS_API_KEY const writeApiKey: string = process.env.NEXT_PUBLIC_MICRO_CMS_WRITE_API_KEY const params = (method: string, data?: {}) => { if (data) { return { "method": method, "headers": { "Content-Type": "application/json; charset=utf-8", "X-WRITE-API-KEY": writeApiKey }, "body": JSON.stringify(data) } } else { return { "method": method, "headers": { "X-API-KEY": apiKey } } } } // 記事を全件取得 export const fetchAllPosts = async (): Promise<Post[]> => { const data = await fetch(`${baseUrl}/blog`, params("GET")) .then(res => res.json()) .catch(() => null) if (data.contents) { return data.contents } } // IDから個別の記事を取得 export const fetchPostById = async (id: string): Promise<Post> => { const data = await fetch(`${baseUrl}/blog/${id}`, params("GET")) .then(res => res.json()) .catch(() => null) if (data) { return data } } // ページ番号によって記事を取得 export const fetchPostsByPageNumber = async (pageNumber: number, limit: number): Promise<Post[]> => { const data = await fetch(`${baseUrl}/blog?offset=${(pageNumber - 1) * 6}&limit=${limit}`, params("GET")) .then(res => res.json()) .catch(() => null) if (data.contents) { return data.contents } } // 最新の記事のみを取得 export const fetchLatestPosts = async (limit: number): Promise<Post[]> => { const data = await fetch(`${baseUrl}/blog?limit=${limit}`, params("GET")) .then(res => res.json()) .catch(() => null) if (data.contents) { return data.contents } } // お問い合わせを作成 export const createContact = async (data: {}) => { await fetch(`${baseUrl}/contacts`, params("POST", data)) } ./src/components/blog/Posts.tsx import { makeStyles } from "@material-ui/core/styles" import Card from "@material-ui/core/Card" import CardMedia from "@material-ui/core/CardMedia" import CardContent from "@material-ui/core/CardContent" import Typography from "@material-ui/core/Typography" import Link from "../utils/Link" const useStyles = makeStyles(() => ({ card: { marginBottom: "0.5rem", transition: "all 0.3s", "&:hover": { boxShadow: "1px 0px 20px -1px rgba(0,0,0,0.2), 0px 0px 20px 5px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)", transform: "translateY(-3px)" } }, cardMedia: { height: 0, paddingTop: "56.25%" } })) interface PostsProps { id: string title: string subTitle: string thumbnail: string } const Posts = ({ id, title, subTitle, thumbnail }: PostsProps) => { const classes = useStyles() return ( <Link href="/blog/[id]" as={`/blog/${id}`}> <Card className={classes.card}> <CardMedia className={classes.cardMedia} image={thumbnail} title={title} /> <CardContent> <Typography variant="h2" gutterBottom> {title} </Typography> <Typography variant="body1" color="textSecondary" component="p"> {subTitle?.length > 140 ? subTitle.substr(0, 140) + "..." : subTitle} </Typography> </CardContent> </Card> </Link> ) } export default Posts ./src/pages/blog/page/[number].tsx import { GetStaticPaths, GetStaticProps } from "next" import { useRouter } from "next/router" import React, { useCallback } from "react" import { makeStyles, ThemeProvider } from "@material-ui/core/styles" import { Container, Grid, Typography } from "@material-ui/core" import { Pagination } from "@material-ui/lab" import Posts from "../../../components/blog/Posts" import PageTemplate from "../../../components/layouts/PageTemplate" import theme from "../../../components/utils/theme" import { fetchAllPosts, fetchPostsByPageNumber } from "../../../lib/api" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem" } })) export const getStaticPaths: GetStaticPaths = async () => { const allPosts = await fetchAllPosts() const per_page = 6 const range = (start: number, end: number) => { return ( [...Array(end - start + 1)].map((_, i) => start + i) ) } const paths = range(1, Math.ceil(allPosts.length / per_page)).map((number) => `/blog/page/${number}`) return { paths, fallback: false } } export const getStaticProps: GetStaticProps = async ({ params }) => { const currentPageNumber: number = Number(params.number) const limit: number = 6 const postsByPageNumber = await fetchPostsByPageNumber(currentPageNumber, limit) const allPosts = await fetchAllPosts() return { revalidate: 1, props: { currentPageNumber, postsByPageNumber, allPosts } } } interface Post { id: string title: string subTitle: string thumbnail: { url: string } } const BlogPage = ({ currentPageNumber, postsByPageNumber, allPosts }) => { const classes = useStyles() const router = useRouter() const handleChangePage = useCallback( (_: React.ChangeEvent<unknown>, number: number) => { router.push(`${number}`) },[router] ) const perPage: number = 6 return ( <> <ThemeProvider theme={theme}> <PageTemplate title="Blog | Corporate Site Sample"> <> <Container maxWidth="lg"className={classes.container}> <Grid container justify="center"> <Grid item> <Typography variant="h1" gutterBottom> Blog </Typography> </Grid> </Grid> </Container> <Container maxWidth="lg"className={classes.container}> <Grid container spacing={4}> {postsByPageNumber?.map((post: Post) => ( <Grid item key={post.id} xs={12} sm={6} md={4}> <Grid container> <Posts id={post.id} title={post.title} subTitle={post.subTitle} thumbnail={post.thumbnail.url} /> </Grid> </Grid> ))} </Grid> </Container> <Container maxWidth="lg"className={classes.container}> <Grid container justify="center"> <Grid item> <Pagination count={Math.ceil(allPosts.length / perPage)} variant="outlined" page={currentPageNumber} onChange={handleChangePage} /> </Grid> </Grid> </Container> </> </PageTemplate> </ThemeProvider> </> ) } export default BlogPage 試しにあと何件か記事を追加してみましょう。 だいぶ良い感じになりました。ちゃんとページネーションも機能していますね。 次は個別記事ページを作成します。 ./src/components/utils/ShareButton.tsx import { FacebookShareButton, FacebookIcon, TwitterShareButton, TwitterIcon, LineShareButton, LineIcon, } from "react-share" interface ShareButtonProps { url: string } const ShareButton = ({ url }: ShareButtonProps) => ( <> <FacebookShareButton url={url} style={{ outline: "none" }}> <FacebookIcon size="32px" round /> </FacebookShareButton> <TwitterShareButton url={url} style={{ marginLeft: `15px`, outline: "none" }} > <TwitterIcon size="32px" round /> </TwitterShareButton> <LineShareButton url={url} style={{ marginLeft: `15px`, outline: "none" }}> <LineIcon size="32px" round /> </LineShareButton> </> ) export default ShareButton ./src/components/blog/Post.tsx import { makeStyles } from "@material-ui/core/styles" import { Container, Grid, Typography } from "@material-ui/core" import ShareButton from "../utils/ShareButton" import moment from "moment" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem", maxWidth: "800px", overflow: "hidden" } })) interface PostProps { id: string title: string publishedAt: string thumbnail: string body: HTMLElement } const Post = ({ id, title, publishedAt, thumbnail, body }: PostProps) => { const classes = useStyles() return ( <> <Container className={classes.container}> <Grid container direction="column" spacing={3}> <Grid item> <Typography variant="h1">{title}</Typography> </Grid> <Grid item> <Typography color="textSecondary"> {moment(publishedAt).format("MMMM Do YYYY")} </Typography> </Grid> <Grid item> <img src={thumbnail} style={{ height: "auto", width: "100%" }} /> </Grid> </Grid> </Container> <Container className={classes.container}> <Grid container direction="column" alignItems="center"> <Grid item > <ShareButton url={`https://<デプロイ後のドメイン>/blog/${id}`} // 適宜変更 /> </Grid> </Grid> </Container> <Container className={classes.container}> <Grid container direction="column" alignItems="center"> <Grid item> <div dangerouslySetInnerHTML={{ __html: `${body}` }} /> </Grid> </Grid> </Container> </> ) } export default Post ./src/pages/blog/[id].tsx import { GetStaticPaths, GetStaticProps } from "next" import { ThemeProvider } from "@material-ui/core/styles" import Post from "../../components/blog/Post" import PageTemplate from "../../components/layouts/PageTemplate" import theme from "../../components/utils/theme" import { fetchAllPosts, fetchPostById } from "../../lib/api" export const getStaticPaths: GetStaticPaths = async () => { const posts = await fetchAllPosts() const paths = posts.map(({ id }) => `/blog/${id}`) return { paths, fallback: false } } export const getStaticProps: GetStaticProps = async ({ params }) => { const id: string = String(params.id) const post = await fetchPostById(id) return { props: { post }, revalidate: 1 } } interface Post { post: { id: string title: string publishedAt: string thumbnail: { url: string } body: HTMLElement } } const BlogId = ({ post }: Post) => { return ( <ThemeProvider theme={theme}> <PageTemplate title="Blog | Corporate Site Sample"> <Post id={post.id} title={post.title} publishedAt={post.publishedAt} thumbnail={post.thumbnail.url} body={post.body} /> </PageTemplate> </ThemeProvider> ) } export default BlogId こんな感じになっていればOKです。 あとはついでにトップページにも掲載しておきましょう。 ./src/pages/index.tsx import { GetStaticProps, NextPage } from "next" import { makeStyles, ThemeProvider } from "@material-ui/core/styles" import { Container, Grid, Typography } from "@material-ui/core" import Slider from "../components/home/Slider" import Introductions from "../components/home/Introductions" import Posts from "../components/blog/Posts" import PageTemplate from "../components/layouts/PageTemplate" import theme from "../components/utils/theme" import { fetchLatestPosts } from "../lib/api" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem" } })) interface Item { name: string description: string color: string } interface Introduction { title: string description: string action: string href: string } interface Post { id: string title: string subTitle: string thumbnail: { url: string } } export const getStaticProps: GetStaticProps = async () => { const latestPosts = await fetchLatestPosts(3) // トップページは最新の3件取得 return { props: { latestPosts }, revalidate: 1 } } const Home = ({ latestPosts }) => { const classes = useStyles() const items: Item[] = [ { name: "Slide1", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", color: "#64ACC8" }, { name: "Slide2", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", color: "#7D85B1" }, { name: "Slide3", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", color: "#CE7E78" } ] const introductions: Introduction[] = [ { title: "Service", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", action: "About Service >", href: "/service" }, { title: "Company", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", action: "About Company >", href: "/company" }, { title: "Recruit", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", action: "About Recruit >", href: "/recruit" } ] return ( <> <ThemeProvider theme={theme}> <PageTemplate title="Home | Corporate Site Sample"> <> <Slider items={items} /> { introductions.map((introduction, index) => ( <Container key={index} maxWidth="lg" className={classes.container}> <Grid container justify={index % 2 == 0 ? "flex-start" : "flex-end"}> <Grid item lg={6} md={6}> <Introductions index={index} title={introduction.title} description={introduction.description} action={introduction.action} href={introduction.href} /> </Grid> </Grid> </Container> )) } <Container maxWidth="lg" className={classes.container}> <Typography variant="h1" align="center" style={{ marginBottom: "2rem" }}> Topics </Typography> <Grid container spacing={4}> {latestPosts?.map((post: Post) => ( <Grid item key={post.id} xs={12} sm={6} md={4}> <Grid container> <Posts id={post.id} title={post.title} subTitle={post.subTitle} thumbnail={post.thumbnail.url} /> </Grid> </Grid> ))} </Grid> </Container> </> </PageTemplate> </ThemeProvider> </> ) } export default Home トップページからも記事を取得できました。 Contact(お問い合わせ)ページを作成 ブログ同様、お問い合わせに関してもmicro CMSを利用させていただきます。 API名: 任意(わかりやすければ何でもOK) エンドポイント: 任意(今回はcontacts) 型: リスト形式 name(名前) テキストフィールド email(メールアドレス) テキストフィールド body(本文) テキストエリア APIスキーマの作成が終わったら、「API設定」→「HTTPメソッド」からPOSTリクエストを有効化します。 また、micro CMSにPOSTリクエストを送る際はX-WRITE-API-KEYが必要になるので、画面の指示に従って作成し、「.env.local」ファイルに追記しておいてください。 /.env.local NEXT_PUBLIC_MICRO_CMS_WRITE_API_KEY=<X-WRITE-API-KEY> ./src/components/Form.tsx import React, { useState } from "react" import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" import Container from "@material-ui/core/Container" import TextField from "@material-ui/core/TextField" import Card from "@material-ui/core/Card" import CardContent from "@material-ui/core/CardContent" import Button from "@material-ui/core/Button" import Box from "@material-ui/core/Box" import Dialog from "@material-ui/core/Dialog" import DialogActions from "@material-ui/core/DialogActions" import DialogContent from "@material-ui/core/DialogContent" import DialogContentText from "@material-ui/core/DialogContentText" import DialogTitle from "@material-ui/core/DialogTitle" import Slide from "@material-ui/core/Slide" import { TransitionProps } from "@material-ui/core/transitions" import { createContact } from "../../lib/api" const Transition = React.forwardRef(function Transition( props: TransitionProps & { children?: React.ReactElement<any, any> }, ref: React.Ref<unknown>, ) { return <Slide direction="up" ref={ref} {...props} /> }) interface CompletionDialogProps { open: boolean handleClose: VoidFunction } // 送信完了したらダイアログを表示 const CompletionDialog = ({ open, handleClose}: CompletionDialogProps) => { return ( <div> <Dialog open={open} TransitionComponent={Transition} keepMounted onClose={handleClose} > <DialogTitle> Thank you for contacting us ! </DialogTitle> <DialogContent> <DialogContentText> Please wait a couple of days for our reply. </DialogContentText> </DialogContent> <DialogActions> <Button onClick={handleClose} color="primary"> Agree </Button> </DialogActions> </Dialog> </div> ) } const useStyles = makeStyles((theme: Theme) => createStyles({ card: { padding: "1rem 4rem" }, header: { marginTop: "1.5rem" }, submitBtn: { margin: theme.spacing(2), textTransform: "none" } }) ) const Form = () => { const classes = useStyles() const [name, setName] = useState("") const [email, setEmail] = useState("") const [body, setBody] = useState("") const [open, setOpen] = useState(false) const handleOpen = () => { setOpen(true) } const handleClose = () => { setOpen(false) } const handleSubmit = (e: any) => { e.preventDefault() const data: {} = { name: name, email: email, body: body } createContact(data) .then(() => { handleOpen() setName("") setEmail("") setBody("") }) .catch((err) => console.log(err)) } return ( <> <Container fixed> <form noValidate autoComplete="off" onSubmit={handleSubmit}> <Card className={classes.card}> <CardContent> <TextField required label="Name" value={name} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)} /> <TextField required fullWidth label="Email" value={email} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)} /> <TextField required fullWidth label="Body" multiline rows={10} value={body} variant="outlined" onChange={(e: React.ChangeEvent<HTMLInputElement>) => setBody(e.target.value)} style={{ marginTop: "2rem"}} /> </CardContent> <Box p={1} textAlign="center"> <Button type="submit" variant="contained" size="large" color="default" disabled={!name || !email || !body ? true : false} className={classes.submitBtn} onClick={handleSubmit} > Send </Button> </Box> </Card> </form> <CompletionDialog open={open} handleClose={handleClose} /> </Container> </> ) } export default Form ./src/pages/contact.tsx import { makeStyles, ThemeProvider } from "@material-ui/core/styles" import { Container, Grid, Typography} from "@material-ui/core" import PageTemplate from "../components/layouts/PageTemplate" import Form from "../components/contact/Form" import theme from "../components/utils/theme" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem", padding: 0 } })) const Contact: React.FC = () => { const classes = useStyles() return ( <> <ThemeProvider theme={theme}> <PageTemplate title="Contact | Sample"> <> <Container maxWidth="lg" className={classes.container}> <Grid container justify="center"> <Grid item> <Typography variant="h1" gutterBottom> Contact </Typography> </Grid> </Grid> </Container> <Container maxWidth="lg" className={classes.container}> <Form /> </Container> </> </PageTemplate> </ThemeProvider> </> ) } export default Contact 試しに送信してみましょう。 micro CMSの管理画面を確認し、先ほどの内容がちゃんと届いていれば成功です。 デプロイ さて、ある程度の形が完成したので本番環境にデプロイしていきましょう。今回は「Vercel」を使います。 あらかじめ適当なGitHubリポジトリを作成し、先ほど作成したコードをプッシュしておいてください。 https://vercel.com/login ↑からGitHubアカウントでログイン。 右上の「New Project」をクリック。 該当のリポジトリをインポートします。 各種環境変数をセットし、右下の「Deploy」を押せばOKです。ビルドが始まるので終わるまで待ちましょう。 しばらくすると「Congratulations!」というメッセージが表示されるはずなので、「Visit」からちゃんと動いているか確認してください。 https://corporate-site-sample.vercel.app Vercelとmicro CMSを連携 最後に、Vercelとmicro CMSの連携を行なっておきます。注意点として、今のままの状態だとmicroCMS側で記事を更新しても本番環境には適用されません。(静的なサイトであるため、再度ビルドしないと差分が反映されない。) そこで、Webhookを利用して記事の新規作成や更新時に自動でビルドを走らせるようする必要があります。 Vercelのダッシュボードを開き、「settgings」→「Git」へ進むと「Deploy Hooks」という項目があるので Hooks名: 任意(わかりやすければ何でもOK) Branch: main(もしくはmaster) それぞれ入力しWebhook URLを発行してください。 今度はmicro CMSの管理画面から「API設定」→「Webhook」へ進み、「カスタム通知」を選択。先ほどのWebhook URLを設定しましょう。 これにて無事連携完了です。心配な方は試しに新しい記事を作成してみてください。 備考 今回作成したコード: https://github.com/kazama1209/corporate-site-sample もし正常に動かない場合、どこが間違っているのか確認するのに使ってください。 あとがき 以上、Next.js × TypeScript × microCMSで簡易的なコーポレートサイトを作ってみました。非常にシンプルな構成なので、あとは各々の好みにカスタマイズしていただければと思います。 今回は画像やアニメーションなどをほとんど入れてないので、その辺を工夫してみるとだいぶ変わるかもしれません。自分はデザイン的なセンスには自信が無いため、煮るなり焼くなり好きにしてください。 デザイン以外の部分で言えば、一応、ブログとお問い合わせの機能に関しては割と参考になるのではないかなと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】useMediaを使ってレスポンシブ対応

概要 use-mediaライブラリを使って、レスポンシブ対応してみます。 各デバイスのサイズに対応するBoolean値(isMobileSite, isTabletSite, isPcSite)を定義してContextを使いアプリケーション全体で共有します。共有されたBoolean値を参照し、各デバイス毎に用意したコンポーネントを出し分けすることでレスポンシブ対応を行います。 環境 react v17.0.2 use-media v1.4.0 typescript v4.1.2 インストール # npmでのインストール $ npm install --save use-media # yarnでのインストール $ yarn add use-media 実装 MediaQueryProvider 下記役割のコンポーネント、カスタムフックを定義してexportします。 デバイスのサイズに対応するBoolean値をContextでアプリケーション全体に共有するためのMediaQueryProvider Providerコンポーネントから共有されたBoolean値を取得するカスタムフックuseMediaQueryContext 各デバイスのブレイクポイントはhttps://hashimotosan.hatenablog.jp/entry/2020/12/06/182327を参考にしました。 Components/Provider/MediaQueryProvider.tsx import React, { createContext, FC, useContext, useMemo } from 'react'; import useMedia from 'use-media'; type Props = { children: React.ReactNode; }; // アプリケーション全体で共有する値の定義 type Context = { // モバイルか? isMobileSite: boolean; // タブレットか? isTabletSite: boolean; // PCか? isPcSite: boolean; }; // Contextの生成 // デフォルトはPCとする。 const MediaQueryContext = createContext<Context>({ isMobileSite: false, isTabletSite: false, isPcSite: true, }); // 各デバイスでのサイズを定義 const mediaQueries = { mobile: '(max-width: 519px)', tablet: '(min-width: 520px) and (max-width: 959px)', pc: '(min-width: 960px)', }; export const MediaQueryProvider: FC<Props> = ({ children }: Props) => { const isMobileSite = useMedia(mediaQueries.mobile); const isTabletSite = useMedia(mediaQueries.tablet); const isPcSite = useMedia(mediaQueries.pc); const value = useMemo(() => ({ isMobileSite, isTabletSite, isPcSite }), [ isMobileSite, isTabletSite, isPcSite, ]); return ( <MediaQueryContext.Provider value={value}> {children} </MediaQueryContext.Provider> ); }; export const useMediaQueryContext = (): Context => useContext(MediaQueryContext); App MediaQueryProviderがメインのコンポーネントを囲うようにします App.tsx import React from 'react'; import MainComponent from 'components/pages/MainComponent'; import { MediaQueryProvider } from 'components/provider/MediaQueryProvider'; export default function App() { return ( <MediaQueryProvider> <MainComponent /> </MediaQueryProvider> ); } MainComponent useMediaQueryContextを使用し、全体に共有されたBoolean値を取得し、Trueの場合に対応するコンポーネントを出力するようにします。 components/pages/MainComponent import React, { FC } from 'react'; import MobileContents from 'components/templates/MobileContents'; import TabletContents from 'components/templates/TabletContents'; import PcContents from 'components/templates/PcContents'; import { useMediaQueryContext } from 'components/provider/MediaQueryProvider'; const MainComponent: FC = () => { const { isMobileSite, isTabletSite, isPcSite } = useMediaQueryContext(); return ( {isMobileSite && ( <MobileContents /> )} {isTabletSite && ( <TabletContents /> )} {isPcSite && ( <PcContents /> )} ); }; export default MainComponent; まとめ 今回はReactでレスポンシブ対応を実装するuseMediaを使ってみました。 各デバイスに対応するコンポーネントを作成することで、簡単にレスポンシブ対応が可能になります。 今後も試してみたライブラリなどを共有していきます。 参考サイト https://github.com/streamich/use-media https://rpf-noblog.com/2021-01-17/react-hook-usemedia/ https://hashimotosan.hatenablog.jp/entry/2020/12/06/182327
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Hooks と TypeScript で簡単 TODO アプリ

公式のチュートリアル以外のチュートリアルを探している人向け。 完成予想図 See the Pen TODO App by React & TypeScript by sprout2000 (@sprout2000_jp) on CodePen. 下準備 create-react-app で TypeScript 用のひな形を準備する。 bash $ npx create-react-app todo --template typescript Need to install the following packages: create-react-app Ok to proceed? (y) Creating a new React app in C:\Users\qiita\Downloads\todo. Installing packages. This might take a couple of minutes. Installing react, react-dom, and react-scripts with cra-template-typescript... ~ snip ~ We suggest that you begin by typing: cd todo yarn start Happy hacking! $ cd todo 手順 簡潔すぎるかもしれない手順とスクリーンショット。 初期状態 Hello. とのみ表示される関数コンポーネント。 src/index.tsx import React from 'react'; import ReactDOM from 'react-dom'; const App: React.FC = () => { return ( <div> <h1>Hello.</h1> </div> ); }; ReactDOM.render(<App />, document.getElementById('root')); create-react-app が自動的にホットリロードしてくれないとき(まれによくある)は、ブラウザのリロードボタンや Ctrl+R を使う。 Todo を入力するフォームを作成 onSubmit や onChange のイベントは preventDefault() してしまっているので特に何も起きない index.tsx const App: React.FC = () => { return ( <div> <form onSubmit={(e) => e.preventDefault()}> <input type="text" value={''} onChange={(e) => e.preventDefault()} /> <input type="submit" value="追加" onSubmit={(e) => e.preventDefault()} /> </form> </div> ); }; フォームに入力された文字列を状態 (=state)として保持する useState フック は現在の state の値と、それを更新するための関数とをペアにした配列を返す useState の引数はそのステートの初期値 src/index.tsx - import React from 'react'; + import React, { useState } from 'react'; import ReactDOM from 'react-dom'; const App: React.FC = () => { + const [text, setText] = useState(''); + return ( <div> <form onSubmit={(e) => e.preventDefault()}> - <input type="text" value={''} onChange={(e) => e.preventDefault()} /> + <input + type="text" + value={text} + onChange={(e) => setText(e.target.value)} + /> <input type="submit" value="追加" onSubmit={(e) => e.preventDefault()} /> </form> </div> ); }; Todo の仕様を考える(その1) タスクの内容として string 型の value というプロパティを持つ。 index.tsx interface Todo { value: string; } const App: React.FC = () => { タスクたち(todos 複数)は Todo 型オブジェクトの配列 とする。 index.tsx const App: React.FC = () => { const [text, setText] = useState(''); + const [todos, setTodos] = useState<Todo[]>([]); return ( onSubmit() イベントで text ステート の内容を todos ステート配列に追加する 配列のステートはそのまま触ってはいけない コピーに対して変更を加えてから更新すること チュートリアル:React の導入 - イミュータビリティは何故重要なのか(公式) todos ステートを更新するコールバック関数を作成する いったん e.preventDefault() しているのは Enter キー打鍵でページそのものがリロードされてしまうのを防ぐため。 src/index.tsx const [todos, setTodos] = useState<Todo[]>([]); // todos ステートを更新する関数 const handleOnSubmit = ( e: React.FormEvent<HTMLFormElement> | React.FormEvent<HTMLInputElement> ) => { e.preventDefault(); // 何も入力されていなかったらリターン if (!text) return; // 新しい Todo を作成 const newTodo: Todo = { value: text, }; /** * スプレッド演算子で todos ステートのコピーへ newTodo を追加する * * 以下と同義 * const oldTodos = todos.slice(); * setTodos(oldTodos.splice(0, 0, newTodo)); * **/ setTodos([newTodo, ...todos]); // フォームへの入力をクリアする setText(''); }; イベントの型がわからない時は、VSCode であればイベント上でマウスカーソルを hover させるとポップアップが表示される。 コールバック関数をイベントに割り当てる コールバックとして渡すのは関数そのもの () => hoge() hoge() のみだと即時に実行されてしまうので用をなさない src/index.tsx return ( <div> - <form onSubmit={(e) => e.preventDefault()}> + <form onSubmit={(e) => handleOnSubmit(e)}> <input type="text" value={text} onChange={(e) => setText(e.target.value)} /> - <input type="submit" value="追加" onSubmit={(e) => e.preventDefault()} /> + <input type="submit" value="追加" onSubmit={(e) => handleOnSubmit(e)} /> </form> </div> フォームへ入力して submit すると ステート (todos) が更新されていることが確認できる。 ステート todos を展開してページに表示する todos (=配列) を非破壊メソッドである Array.prototype.map() を使って <li></li> タグへ展開する。 src/index.tsx <div> <form onSubmit={(e) => handleOnSubmit(e)}> <input type="text" value={text} onChange={(e) => setText(e.target.value)} /> <button onClick={() => handleOnClick()}>追加</button> </form> <ul> {todos.map((todo) => { return <li>{todo.value}</li>; })} </ul> </div> これだけでは各 <li> に key が設定されていないため、以下のような警告が表示されてしまう。 チュートリアル:React の導入 - key を選ぶ(公式) Todo の仕様を考える(その2) それぞれの Todo (タスク)に一意な ID を与える必要があるため、number 型のプロパティを Todo 型に追加する。 src/index.tsx interface Todo { value: string; id: number; } handleOnSubmit() メソッドを更新。 src/index.tsx const handleOnSubmit = ( e: React.FormEvent<HTMLFormElement> | React.FormEvent<HTMLInputElement> ) => { e.preventDefault(); if (!text) return; const newTodo: Todo = { value: text, + id: new Date().getTime(), }; setTodos([newTodo, ...todos]); setText(''); }; <li></li> タグに key (=id) を付加する。 src/index.tsx <ul> {todos.map((todo) => { return <li key={todo.id}>{todo.value}</li>; })} </ul> 登録済みの todo を編集可能にする todo.value を <input /> タグでラップする。 src/index.tsx <ul> {todos.map((todo) => { return ( <li key={todo.id}> <input type="text" value={todo.value} onChange={(e) => e.preventDefault()} /> </li> ); })} </ul> ここでも、とりあえず e.preventDefault() しているので入力しても何も起きない。 登録済み todo が編集された時のコールバック関数を作成 どの todo が編集されたのか特定するため、その todo の id を引数として受け取る e.target.value を書き換え後の todo.value の値とするために第2引数として受け取る src/index.tsx const handleOnEdit = (id: number, value: string) => { /** * 引数として渡された todo の id が一致する * todos ステート(のコピー)内の todo の * value プロパティを引数 value に書き換える */ const newTodos = todos.map((todo) => { if (todo.id === id) { todo.value = value; } return todo; }); // todos ステートを更新 setTodos(newTodos); }; 上のコールバック関数を <input onChange={} /> に割り当てる。 src/index.tsx <ul> {todos.map((todo) => { return ( <li key={todo.id}> <input type="text" value={todo.value} - onChange={(e) => e.preventDefault()} + onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> </li> ); })} </ul> タスクの完了/未完了を操作できるようにする Todo の仕様を考える(その3) Todo 型に完了/未完了を示すプロパティを追加する 完了/未完了 (= yes or no) を表すので型は Boolean 型 src/index.tsx interface Todo { value: string; id: number; + checked: boolean; } handleOnSubmit() メソッドを更新する。 src/index.tsx if (!text) return; const newTodo: Todo = { value: text, id: new Date().getTime(), + checked: false, }; setTodos([newTodo, ...todos]); それぞれの todo の前へチェックボックスを置く。 index.tsx <ul> {todos.map((todo) => { return ( <li key={todo.id}> <input type="checkbox" checked={todo.checked} onChange={(e) => e.preventDefault()} /> <input type="text" value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> </li> ); })} </ul> チェックボックスがチェックされたときのコールバック関数を作成する 上の handleOnEdit() とパターンは同じ。 src/index.tsx const handleOnCheck = (id: number, checked: boolean) => { const newTodos = todos.map((todo) => { if (todo.id === id) { todo.checked = !checked; } return todo; }); setTodos(newTodos); }; チェックボックスのイベントへ紐付ける。 src/index.tsx return ( <li key={todo.id}> <input type="checkbox" checked={todo.checked} onChange={() => handleOnCheck(todo.id, todo.checked)} /> <input type="text" value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> </li> ); このままではチェック済みのタスクも編集できてしまうので、チェック済みの項目は入力フォームを無効化する。 src/index.tsx <input type="text" + disabled={todo.checked} value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> 登録済みの todo を削除可能にする 入力フォームの後ろへ削除ボタンを追加する。 index.tsx return ( <li key={todo.id}> <input type="checkbox" checked={todo.checked} onChange={() => handleOnCheck(todo.id, todo.checked)} /> <input type="text" disabled={todo.checked} value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> <button onClick={() => console.log('removed!')}>削除</button> </li> ); Todo の仕様を考える(その4) checked の場合と同様に removed というフラグを追加する。 src/index.tsx interface Todo { value: string; id: number; checked: boolean; removed: boolean; } handleOnSubmit() メソッドを更新する。 src/index.tsx if (!text) return; const newTodo: Todo = { value: text, id: new Date().getTime(), checked: false, removed: false, }; setTodos([newTodo, ...todos]); 削除ボタンがクリックされたときのコールバック関数を作成する これも handleOnEdit() や handleOnChecked() と同じパターン。 src/index.tsx const handleOnRemove = (id: number, removed: boolean) => { const newTodos = todos.map((todo) => { if (todo.id === id) { todo.removed = !removed; } return todo; }); setTodos(newTodos); }; todo.removed のフラグによってボタンのラベルを入れ替える。 src/index.tsx <button onClick={() => handleOnRemove(todo.id, todo.removed)}> {todo.removed ? '復元' : '削除'} </button> 削除されたアイテムのチェックボックスと入力フォームも無効化する。 src/index.tsx <input type="checkbox" disabled={todo.removed} checked={todo.checked} onChange={() => handleOnCheck(todo.id, todo.checked)} /> <input type="text" disabled={todo.checked || todo.removed} value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> タスクをフィルタリングする機能を追加する このままでは削除済みのアイテムもそのまま表示されてしまうので、タスクをフィルタリングする機能を追加する。 フィルタリングするセレクタを作成 ここでも onChange はとりあえずダミー。 src/index.tsx <div> <select defaultValue="all" onChange={(e) => e.preventDefault()}> <option value="all">すべてのタスク</option> <option value="checked">完了したタスク</option> <option value="unchecked">未完了のタスク</option> <option value="removed">削除済みのタスク</option> </select> <form onSubmit={(e) => handleOnSubmit(e)}> ~ snip ~ </div> 現在のフィルターを格納する filter ステートを追加する filter は4種類とする。 src/index.tsx type Filter = 'all' | 'checked' | 'unchecked' | 'removed'; 前項の <option /> タグの値を Filter 型のステート として保持する。 src/index.tsx const App: React.FC = () => { const [text, setText] = useState(''); const [todos, setTodos] = useState<Todo[]>([]); + const [filter, setFilter] = useState<Filter>('all'); セレクタの onChange イベントで filter ステートを更新する Filter を単なる string 型 にすれば下のようなキャストは不要だが、次項の switch 文で型によるエディタの補完を享受するため、あえて Filter 型 を利用する。 src/index.tsx <select defaultValue="all" - onChange={(e) => e.preventDefault()}> + onChange={(e) => setFilter(e.target.value as Filter)}> <option value="all">すべてのタスク</option> <option value="checked">完了したタスク</option> <option value="unchecked">未完了のタスク</option> <option value="removed">削除済みのタスク</option> </select> フィルタリング後の Todo 型の配列を返す関数を用意する <ul></ul> タグの中で展開されている todos ステート をタグへ渡す前に加工する 現在の filter ステート に応じて Todo 型配列 の要素をフィルタリングする Array.prototype.filter() メソッドも非破壊メソッド src/index.tsx const filteredTodos = todos.filter((todo) => { switch (filter) { case 'all': return !todo.removed; case 'checked': return todo.checked && !todo.removed; case 'unchecked': return !todo.checked && !todo.removed; case 'removed': return todo.removed; default: return todo; } }); todos ステートを展開する <ul></ul> タグにフィルタリング済みのリストを渡す src/index.tsx <ul> - {todos.map((todo) => { + {filteredTodos.map((todo) => { return ( <li key={todo.id}> <input type="checkbox" disabled={todo.removed} 「削除済みのタスク」や「完了済みのタスク」が表示されている時は、入力フォームは無効化する。 src/index.tsx <form onSubmit={(e) => handleOnSubmit(e)}> <input type="text" value={text} + disabled={filter === 'checked' || filter === 'removed'} onChange={(e) => setText(e.target.value)} /> <input type="submit" + disabled={filter === 'removed' || filter === 'checked'} value="追加" onSubmit={(e) => handleOnSubmit(e)} /> </form> 「ゴミ箱を空にする」ボタンを作成する フィルターで「削除済み」の Todo リストを表示しているときには、削除済みタスクを完全に消去できるようにする。 フィルターが「削除済み」の場合は「ゴミ箱を空にする」するボタンを表示し、それ以外のときは従前の入力フォームを表示する。 src/index.tsx <option value="removed">削除済みのタスク</option> </select> {filter === 'removed' ? ( <button onClick={() => console.log('remove all')}> ゴミ箱を空にする </button> ) : ( <form onSubmit={(e) => handleOnSubmit(e)}> <input type="text" value={text} disabled={filter === 'checked' || filter === 'removed'} onChange={(e) => setText(e.target.value)} /> <input type="submit" value="追加" disabled={filter === 'checked' || filter === 'removed'} onSubmit={(e) => handleOnSubmit(e)} /> </form> )} <ul> {filteredTodos.map((todo) => { こうなると入力フォームが描画される場合には filter === 'removed' という状態が発生し得ないので、入力フォームからこれらを削除する。 src/index.tsx {filter === 'removed' ? ( <button onClick={() => console.log('remove all')}> ゴミ箱を空にする </button> ) : ( <form onSubmit={(e) => handleOnSubmit(e)}> <input type="text" value={text} disabled={filter === 'checked'} onChange={(e) => setText(e.target.value)} /> <input type="submit" value="追加" disabled={filter === 'checked'} onSubmit={(e) => handleOnSubmit(e)} /> </form> )} 「ゴミ箱を空にする」コールバック関数を作成する todos ステート配列から removed フラグが立っている要素を取り除くのみなので、これまでと同様のパターンで処理すればいい。 src/index.tsx const handleOnEmpty = () => { const newTodos = todos.filter((todo) => !todo.removed); setTodos(newTodos); } src/index.tsx {filter === 'removed' ? ( + <button onClick={() => handleOnEmpty()}>ゴミ箱を空にする</button> ) : ( <form onSubmit={(e) => handleOnSubmit(e)}> <input type="text" value={text} disabled={filter === 'checked'} onChange={(e) => setText(e.target.value)} /> <input type="submit" value="追加" disabled={filter === 'checked'} onSubmit={(e) => handleOnSubmit(e)} /> </form> )} ゴミ箱が空の場合(= removed フラグが立っているタスクが todos ステート配列に存在しない)には、ボタンを無効化する。 src/index.tsx <button onClick={() => handleOnEmpty()} disabled={todos.filter((todo) => todo.removed).length === 0} > ゴミ箱を空にする </button> さらなる改良のヒント CSS フレームワークなどを利用してルック&フィールを洗練させよう Web ストレージを利用して Todo リストがリロード後も保持されるようにしよう ドラッグ&ドロップで Todo リストの順番を並び替えられるようにしよう 参照
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Kawaii がかわいい

React Kawaii という React コンポーネントのお試し。 10 Trending projects on GitHub for web developers - 23rd April 2021 で紹介されていたもの。 React Kawaii 使い方 以下で。 npm install --save react-native-svg npm install --save react-kawaii かわいいのは題名のとおりさておいて、React ってこういう感じなのかあ というのが分かるところが面白かったかもしれない。 書いたのこれだけ。 import { Planet } from 'react-kawaii' import { Cat } from 'react-kawaii' import { Ghost } from 'react-kawaii' //中略 <Planet size={90} mood="blissful" color="#FDA7DC" /> <Planet size={90} mood="lovestruck" color="#FDA7DC" /> <Planet size={90} mood="blissful" color="#FDA7DC" /> <Cat size={110} mood="excited" color="#596881" /> <Cat size={110} mood="happy" color="#596881" /> <Cat size={110} mood="excited" color="#596881" /> <Ghost size={100} mood="excited" color="#83D1FB" /> <Ghost size={100} mood="happy" color="#83D1FB" /> <Ghost size={100} mood="ko" color="#83D1FB" /> こうなる React メモ 以上お楽しみいただければさいわいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【未経験者向け】Rails api×React×Dockerで開発環境構築

Rails×React×Dockerにて開発環境を構築したので、アウトプット用に残しておきます。 下記の記事を参考にして構築していきました。 DockerでRuby on Rails + Reactを別々にアプリ作成する環境構築手順 Docker使ってReact × Rails(API)の環境構築 前提知識の確認 自分は初学者でrailsでしかWebアプリを構築した事がなかったのですが、Dockerで開発環境を構築する場合は、 rails newをする前に、いくつかのファイルを用意する必要があります。 ※詳しくはこちらが分かり易い Docker 公式ドキュメントの Rails Quickstart 完全解説 簡単に伝えると、プロジェクトのディレクトを作成した後rails newをする前にDocker環境構築用のファイルをいくつか作成した後で、 rails newやcreate-react-app行う。 フォルダ構造は下記のような形からスタートする アプリ名 backend Dockerfile entrypoint.sh Gemfile Gemfile.lock frontend Dockerfile docker-compose.yml ①ファイルを用意していく 上記構造のディレクトリとファイルを作成したら、中身を記述していく。 ※アプリ名と記載がある部分は、ご自分で作成中のアプリ名に変更して下さい。 docker-compose.yml docker-compose.yml version: "3" services: db: image: mariadb command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci environment: MYSQL_DATABASE: "アプリ名_development" MYSQL_ROOT_PASSWORD: "password" volumes: - mysql-data:/var/lib/mysql/data - /tmp/dockerdir:/etc/mysql/conf.d/ ports: - 3306:3306 backend: build: context: ./backend/ dockerfile: Dockerfile command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3001 -b '0.0.0.0'" volumes: - .:/アプリ名 ports: - "3001:3001" depends_on: - db frontend: build: context: ./frontend/ dockerfile: Dockerfile volumes: - ./frontend:/usr/src/app/frontend working_dir: /usr/src/app/frontend command: sh -c "npm start --host 0.0.0.0 --port 3000" ports: - "3000:3000" stdin_open: true volumes: mysql-data: {} dbには、mysqlを使用しております。 rails(backend)側では、3001番ポートで立ち上がるよう設定しており、react(frontend)側では3000番ポートにしてあります。 docker-compose.yml backend: build: ports: - "3001:3001" frontend: build: ports: - "3000:3000" rails側のファイル用意(全部で4つ) Dockerfile entrypoint.sh Gemfile Gemfile.lock Dockerfile(アプリ名/backend/Dockerfile) Dokcerfile. FROM ruby:2.7.2 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs RUN mkdir /アプリ名 WORKDIR /アプリ名 COPY Gemfile /アプリ名/Gemfile COPY Gemfile.lock /アプリ名/Gemfile.lock RUN bundle install COPY . /アプリ名 # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3001 # Start the main process. CMD ["rails", "server", "-b", "0.0.0.0"] Gemfile Gemfile. source 'https://rubygems.org' gem 'rails', '5.2.5' railsのバージョンは5.2.5にしてあります。 最近発生していたmimemagi関連のエラーを回避する為ですが、 rails6系が良い方は、6.0.3.6又は6.1.3.1であれば、エラー回避出来るようなので、どちらかを指定して下さい。 ※詳しくは下記を参照下さい RailsのGPL混入問題についてまとめ(mimemagic) Gemfile.lock touch Gemfile.lockで作成したら、空ファイルのままで大丈夫です。 entrypoint.sh entrypoint.sh #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /アプリ名/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@" これで、rails側の準備はOKです。 react側のファイル用意 Dockerfile(アプリ名/frontend/Dockerfile) FROM node:14.15.1-alpine ※この記述だけで大丈夫です。 ②Dockerコマンドを実行する 順番に実行して下さい。 アプリ名 $ docker-compose run backend rails new . --force --no-deps --database=mysql --api アプリ名 $ docker-compose build アプリ名 $ docker-compose run --rm frontend sh -c "npm install -g create-react-app && create-react-app frontend" 成功すると、見慣れたディレクトリ構造になってるかと思います。 database.yml(backend/config/database.yml)を修正する database.yml default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: 自分で設定 password: "自分で設定" host: db socket: /var/run/mysqld/mysqld.sock development: <<: *default database: アプリ名_development test: <<: *default database: アプリ名_test usernameとpasswordは、お好きに設定して下さい。mysqlをターミナルから使用する際に求められます。 再びDockerコマンドを実行 アプリ名 $ docker-compose up アプリ名 $ docker-compose run api rake db:create 下記の画面が出ていれば、成功 rails(localhost:3001) react(localhost:3000) 最後に 記事をまとめるのは、難しいですね・・・ 初めてなので、稚拙な部分も沢山あったかと思いますが、 不明点や記述の仕方に対するフィードバックが御座いましたら、お教え頂けますと幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactで複数オブジェクトを配列にまとめたデータをmapメソッドで表示する

はじめに 半日費やして解決できた実装 オブジェクトや配列の特徴などをなんとなくでしか理解していなかったため嵌った メモとして残す やりたかったこと 親コンポーネントで管理するState app.jsx const [post,setPost] = useState([ {title:'Topic1', text:'Hello world'}, {title:'Topic2', text:'Hello world'}, {title:'Topic3', text:'Hello world'}, {title:'Topic4', text:'Hello world'}, {title:'Topic5', text:'Hello world'}, ]) 記事のタイトルと本文と仮定したこのstateをList形式でオブジェクトの数だけ子コンポーネントで表示したい つまりulの直下にmapメソッドでオブジェクトの数だけli要素を作り、title.textそれぞれの値をさらに下のコンポーネントにpropsとして渡す 実装 List.jsx const List = () => { const state = useContext(PostContext); return ( <div> <ListUI> {state.map((s) => { return( <ListItem key={s.title}> <Items title={s.title} text={s.text}/> </ListItem> ) })} </ListUI> </div> ); }; hooksのusuContextで親コンポーネントのstateを受け取りmapメソッドでオブジェクトの数だけ回す material UIのListItemでli要素を作り、titleとtextの値をそれぞれさらに子供のコンポーネントへpropsとして渡した。 Items.jsx const Items = (props) => { return ( <div> <h1>{props.title}</h1> <p>{props.text}</p> </div> ); }; watasareta propsをこのように実装すると できた つまずいた点 正直オブジェクトや配列に関する知識が甘く、またmapを使うべきかforEachを使うべきか判断も自信がなくなんとか実装しようと右往左往してしまった。 またconsoleには反映されるのに画面には描画されない現象に手こずった mapメソッドの後にreturnを書き忘れていたことが原因であった。 mapの結果をListItem>Itemsのように複数階層で表示したい場合はreturnは必須 おわりに カタルシス
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む