Next.js+Tailwind CSS+Storybookでフロントエンド開発④

はじめに

前回まで

APIの呼び出し準備まで完了しました。
今回はコンポーネントを組み合わせて、レイアウトを構築していこうと思います。
APIも使います!

  1. 環境構築
  2. Storybookで管理しながらコンポーネントを作成
  3. APIの呼び出し
  4. コンポーネントを組み合わせてレイアウト構築

完成図

用意したコンポーネントを組み合わせて、完成図に近しいレイアウトを組んでいきます。

レイアウトを組んでいく

layout.tsx

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

"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>
    );
}

長いので分割して説明したいと思います

一括抽選、リセット

    const drawAll = () => {
        setDrawTrigger(true);
        setDisabledDrawAll(true);
    };
    const resetAll = () => {
        setResetTrigger((prev) => prev + 1);
        setDrawTrigger(false);
        setDisabledDrawAll(false);
    };

一括抽選、リセットの処理です。
リセットを押すごとにresetTriggerの数値が増やし、Panelコンポーネント側で検知してもらおうという考えです。

APIの呼び出し部分

    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部分

    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 でのアプリ作成は完了です。

とても長くなってしまいましたが、ここまでお付き合いいただきありがとうございました!