目次
はじめに
前回まで
APIの呼び出し準備まで完了しました。
今回はコンポーネントを組み合わせて、レイアウトを構築していこうと思います。
APIも使います!
- 環境構築
- Storybookで管理しながらコンポーネントを作成
- APIの呼び出し
- コンポーネントを組み合わせてレイアウト構築
完成図
用意したコンポーネントを組み合わせて、完成図に近しいレイアウトを組んでいきます。
レイアウトを組んでいく
layout.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "../css/globals.css"; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="ja"> <body className={`${geistSans.variable} ${geistMono.variable} antialiased h-screen`}> {children} </body> </html> ); } |
に下記のクラスを設定しました。
描画パーツが少ないので、画面の高さが狭まらないようにしたかったからです。
- h-screen →
height: 100vh
page.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
"use client"; import React, { useState } from "react"; import axios from "axios"; import { Header } from "../../components/header/Header"; import { Select } from "../../components/select/Select"; import { Input } from "../../components/input/Input"; import { Panel } from "../../components/panel/Panel"; import { Button } from "../../components/button/Button"; export default function Home() { const selectOoptions = [ { key: "digit7", value: "7", label: "7桁" }, { key: "digit6", value: "6", label: "6桁" }, { key: "digit5", value: "5", label: "5桁" }, { key: "digit4", value: "4", label: "4桁" }, { key: "digit3", value: "3", label: "3桁" }, { key: "digit2", value: "2", label: "2桁" }, { key: "digit1", value: "1", label: "1桁" }, ]; const [min, setMin] = useState("0"); const [max, setMax] = useState("9"); const [digit, setDegit] = useState(selectOoptions[0].value); const [drawTrigger, setDrawTrigger] = useState(false); const [disabledDrawAll, setDisabledDrawAll] = useState(false); const [resetTrigger, setResetTrigger] = useState(0); const drawAll = () => { setDrawTrigger(true); setDisabledDrawAll(true); }; const resetAll = () => { setResetTrigger((prev) => prev + 1); setDrawTrigger(false); setDisabledDrawAll(false); }; const onInputMinChange = (newInput: string) => { setMin(newInput); }; const onInputMaxChange = (newInput: string) => { setMax(newInput); }; const onSelectChange = (newDegit: string) => { setDegit(newDegit); }; const url = `/api/number?min=${min}&max=${max}&count=${digit}`; const getNumber = async () => { try { const response = await axios.get(url); return response.data[0]; } catch (error) { console.error("数字の取得に失敗しました:", error); return null; } }; return ( <div className="w-full h-full flex flex-col"> <Header /> <main className="grow grid justify-items-center items-center"> <div className="grid justify-items-center items-center gap-20"> <div className="grid grid-cols-3 gap-4"> <Select name="digit" label="桁数" options={selectOoptions} defaultValue={selectOoptions[0].value} onSelectChange={onSelectChange} /> <Input name="min" label="最小値" defaultValue={min} onInputChange={onInputMinChange} /> <Input name="max" label="最大値" defaultValue={max} onInputChange={onInputMaxChange} /> </div> <div className="flex gap-4"> {Array.from({ length: parseInt(digit, 10) }).map((_, index) => ( <Panel key={`panel${index}`} getNumber={getNumber} drawTrigger={drawTrigger} resetTrigger={resetTrigger} /> ))} </div> <div className="flex gap-8"> <Button primary={true} disabled={disabledDrawAll} label="一括抽選" size="large" onClick={drawAll} /> <Button primary={false} label="リセット" size="large" onClick={resetAll} /> </div> </div> </main> </div> ); } |
長いので分割して説明したいと思います
一括抽選、リセット
1 2 3 4 5 6 7 8 9 |
const drawAll = () => { setDrawTrigger(true); setDisabledDrawAll(true); }; const resetAll = () => { setResetTrigger((prev) => prev + 1); setDrawTrigger(false); setDisabledDrawAll(false); }; |
一括抽選、リセットの処理です。
リセットを押すごとにresetTrigger
の数値が増やし、Panelコンポーネント側で検知してもらおうという考えです。
APIの呼び出し部分
1 2 3 4 5 6 7 8 9 10 |
const url = `/api/number?min=${min}&max=${max}&count=${digit}`; const getNumber = async () => { try { const response = await axios.get(url); return response.data[0]; } catch (error) { console.error("数字の取得に失敗しました:", error); return null; } }; |
前回用意したAPIルートの/api/number
にリクエストさせています。
JSX部分
1 2 3 4 5 |
return ( <div className="w-full h-full flex flex-col"> <Header /> <main className="grow grid justify-items-center items-center"> {/* 省略 */} |
<Header />
意外は
クラスを設定しました。の領域に広げたかったので、
grow
- grow →
flex-grow: 1
動作確認
ざっと下記を確認してみます
- 1つずつ抽選する
- 桁数を変更する
- 最小値を変更する
- 最大値を変更する
- 一括抽選する
- リセットする
(動画はカクついてるけど)想定どおりに動作しました!
おわりに
以上でNext.js+Tailwind CSS+Storybook でのアプリ作成は完了です。
とても長くなってしまいましたが、ここまでお付き合いいただきありがとうございました!