0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LEMP環境でNext.js(TypeScript)とLaravel(PHP)を利用して、勤怠管理アプリを作成してみる。〜最終工程編〜

Last updated at Posted at 2025-01-26

前回は、退会ページの作成をし、勤怠管理アプリのすべてのページのコンポーネントを作成しました。

ここまでは、ただコンポーネントを作成しただけです。

次は、この作成したコンポーネントを機能させるためのファイルを作成します。

その前に、グローバルナビゲーションは、ユーザー情報があるかないかで表示される内容が異なるので、

ユーザー情報を渡すための関数をfrontendディレクトリ内のsrcディレクトリ内にhooksというディレクトリを作成し、

useAuth.tsというファイルを作成します。

frontend/src/hooks/useAuth.ts
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useMutation } from 'react-query';
import { Me } from '../types';

// APIを呼び出すための関数
const loginUser = async ({ userId, password }: { userId: string; password: string }) => {
    const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/login`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ userId, password }), // JSON形式で送信
    });

    if (!response.ok) {
        throw new Error('ログインに失敗しました。');
    }

    return response.json();
};

const useAuth = (): {
    user: Me | null;
    loading: boolean;
    errorMessage: string | null;
    login: (userId: string, password: string) => void;
} => {
    const [user, setUser] = useState<Me | null>(null); // ユーザー情報を保持
    const [errorMessage, setErrorMessage] = useState<string | null>(null); // エラーメッセージの保持
    const router = useRouter();

    const mutation = useMutation(loginUser, {
        onMutate: () => {
            setErrorMessage(null);
        },
        onSuccess: (data) => {
            setUser(data.user); // 成功時ユーザー情報をセット
        },
        onError: (error: any) => {
            setErrorMessage(error.message || 'ログインに失敗しました。'); // エラーメッセージを表示
        },
    });

    // ログインリクエストを外部から呼び出す関数
    const login = (userId: string, password: string) => {
        mutation.mutate({ userId, password });
    };

    // ユーザーが null の場合、ログインページへリダイレクト
    useEffect(() => {
        if (user === null && router.pathname !== '/login') {
            router.push('/login');
        }
    }, [user, router]);

    return {
        user,
        loading: mutation.isLoading,
        errorMessage,
        login,
    };
};

export default useAuth;


useMutationという
react-query を使用するには、QueryClientProvider を使って QueryClient をコンテキストとしてアプリ全体に渡す必要があります。

QueryClient の作成

const queryClient = new QueryClient(); 

で QueryClient をインスタンス化します。

これにより、クエリやミューテーションの管理が可能になります。

QueryClientProvider.tsx の作成

まず、React Query の QueryClientProvider を

appディレクトリ内にprovidersディレクトリを作成し、QueryClientProvider.tsxを作成し、そこに定義します。

frontend/src/app/providers/QueryClientProvider.tsx
"use client";

import { QueryClient, QueryClientProvider } from "react-query";
import { ReactNode, useState } from "react";

export default function ReactQueryProvider({ children }: { children: ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());

  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}


useState を使うことで QueryClient インスタンスがコンポーネントの再レンダリングごとに作成されるのを防ぎます。

もし、childrenにエラーが出る場合、
tsconfig.jsonファイルを

frontend/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
+ },
+ "typeRoots": [
+ "./node_modules/@types",  
+ "./node_modules/react-query"  
+ ],
+ "types": ["react", "react-dom"]  // ここに追加
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

してください。

変更後に node_modules を削除して再インストールしてください。

$ rm -rf node_modules package-lock.json
$ npm install

layout.tsx に QueryClientProvider を適用

次に、layout.tsxを以下のように変更し、 QueryClientProvider をラップ します。

frontend/src/app/layout.tsx
import "../globals.css";
import ReactQueryProvider from "./providers/QueryClientProvider";
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "勤怠管理アプリ",
  description: "Next.js & Laravel を使った勤怠管理アプリ",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <ReactQueryProvider>{children}</ReactQueryProvider>
      </body>
    </html>
  );
}

page.tsx で useQuery を使う

page.tsx で useQuery を使うには、クライアントコンポーネント ("use client") にする必要があります。

また、appディレクトリ内にapiディレクトリを作成し、auth.tsファイルを作成します。

そして、API ルートに関するコードを書いていきます。

frontend/src/app/api/auth.ts
import { NextApiRequest, NextApiResponse } from "next";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === "GET") {
    res.status(200).json({ message: "認証API" });
  } else {
    res.status(405).json({ message: "認証APIエラー" });
  }
}

Next.jsを動かした際、ブラウザで、(http://localhost:3000) または、(http://localhost:8080(Dockerを起動させた時)と入力したとき、 エントリーポイントとなるファイルは、frontend/src/app/page.tsxです。

なので、

frontend/src/app/page.tsx
import Image from "next/image";

export default function Home() {
  return (
    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
      <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
        <Image
          className="dark:invert"
          src="/next.svg"
          alt="Next.js logo"
          width={180}
          height={38}
          priority
        />
        <ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
          <li className="mb-2">
            Get started by editing{" "}
            <code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
              src/app/page.tsx
            </code>
            .
          </li>
          <li>Save and see your changes instantly.</li>
        </ol>

        <div className="flex gap-4 items-center flex-col sm:flex-row">
          <a
            className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
            target="_blank"
            rel="noopener noreferrer"
          >
            <Image
              className="dark:invert"
              src="/vercel.svg"
              alt="Vercel logomark"
              width={20}
              height={20}
            />
            Deploy now
          </a>
          <a
            className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
            href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
            target="_blank"
            rel="noopener noreferrer"
          >
            Read our docs
          </a>
        </div>
      </main>
      <footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
        <a
          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          <Image
            aria-hidden
            src="/file.svg"
            alt="File icon"
            width={16}
            height={16}
          />
          Learn
        </a>
        <a
          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          <Image
            aria-hidden
            src="/window.svg"
            alt="Window icon"
            width={16}
            height={16}
          />
          Examples
        </a>
        <a
          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
          href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          <Image
            aria-hidden
            src="/globe.svg"
            alt="Globe icon"
            width={16}
            height={16}
          />
          Go to nextjs.org →
        </a>
      </footer>
    </div>
  );
}

というコードを変えないといけません。

「▲ Deploy now」という部分だけ残し、書き換えるので、他のボタンや要素は、削除します。

しかし、現在、 <main> のクラスが sm:items-start になっているため、sm (小さい画面以上) では左寄せになってしまいます。

なので、 sm:items-start を削除し items-center のみにする

<main className="flex flex-col gap-8 row-start-2 items-center">

text-center を追加

リスト (<ol>) text-left が原因で、リスト内の文章が左寄せになっています。
sm:text-left を削除します。


<ol className="list-inside list-decimal text-sm text-center font-[family-name:var(--font-geist-mono)]">

ボタンを中央寄せ

そして、ボタンの <div>sm:flex-row があるため、小さい画面では縦並び、大きい画面では横並び になっています。

なので、sm:flex-row を削除しjustify-center を追加

<div className="flex gap-4 items-center flex-col justify-center">

ボタンの色を青緑色にしたいので、Tailwind CSS の teal 系の色 (teal-500) を使います。

また、ホバー時に濃い青緑にするために teal-600 を設定します。

bg-foreground text-backgroundbg-teal-500 text-white
hover:bg-[#383838] dark:hover:bg-[#ccc]hover:bg-teal-600

これらとAPIなどを踏まえると、コードを下のように変えていきます。

frontend/src/app/page.tsx
"use client";

import Image from "next/image";
import { useQuery } from "react-query";

async function fetchData() {
  const res = await fetch("/api/auth"); // APIエンドポイントを変更
  if (!res.ok) throw new Error("データの取得に失敗しました");
  return res.json();
}

export default function Home() {
  const { data, error, isLoading } = useQuery("auth", fetchData);

  return (
    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
      <main className="flex flex-col gap-8 row-start-2 items-center">
        <Image
          className="dark:invert"
          src="/next.svg"
          alt="Next.js logo"
          width={180}
          height={38}
          priority
        />
        {isLoading && <p>読み込み中...</p>}
        {error && <p>エラーが発生しました</p>}
        {data && <p>取得データ: {JSON.stringify(data)}</p>}
        <ol className="list-inside list-decimal text-sm text-center font-[family-name:var(--font-geist-mono)]">
          <li className="mb-2">
            自分で勤怠管理アプリ作ってみました。{" "}
            <code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
              Next.js and Laravel
            </code>
          </li>
        </ol>

        <div className="flex gap-4 items-center flex-col justify-center">
          <a
            className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-teal-500 text-white gap-2 hover:bg-teal-600 text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
            href="/login"
            target="_blank"
            rel="noopener noreferrer"
          >
            <Image
              className="dark:invert"
              src="/vercel.svg"
              alt="Vercel logomark"
              width={20}
              height={20}
            />
            アプリを始める
          </a>
        </div>
      </main>
    </div>
  );        
}

この構成にすれば、全てのページでreact-queryを統一して使用にします。

その後、以下のようにディレクトリとファイルを作成し、作成したコンポーネントを貼り付けていきます。

frontend/src/app/login/page.tsx
import { NextPage } from 'next/types';
import Login from '@/components/pages/Login';
import useAuth from '@/hooks/useAuth';

const Index: NextPage = () => {
  const { loading, user } = useAuth();
  
  if (loading) {
    return <div>処理中...</div>
  }

  if (!user) {
    return <div>ユーザーが見つかりませんでした。</div>
  }

  return <Login/>;
};

export default Index;

frontend/src/app/leave/page.tsx

import { NextPage } from 'next/types';
import Leave from '@/components/pages/Leave';
import useAuth from '@/hooks/useAuth';

const Index: NextPage = () => {
  const { loading, user } = useAuth();
  
  if (loading) {
    return <div>処理中...</div>
  }

  if (!user) {
    return <div>ユーザーが見つかりませんでした</div>
  }

  return <Leave/>;
};

export default Index;
frontend/src/app/leave/confirm/page.tsx
import { NextPage } from 'next/types';
import LeaveConfirm from '@/components/pages/Leave/Confirm';
import useAuth from '@/hooks/useAuth';

const Index: NextPage = () => {
  const { loading, user } = useAuth();
  
  if (loading) {
    return <div>処理中...</div>
  }

  if (!user) {
    return <div>ユーザーが見つかりませんでした</div>
  }

  return <LeaveConfirm/>;
};

export default Index;
frontend/src/app/leave/complete/page.tsx
import { NextPage } from 'next/types';
import LeaveComplete from '@/components/pages/Leave/Complete';
import useAuth from '@/hooks/useAuth';

const Index: NextPage = () => {
  const { loading, user } = useAuth();
  
  if (loading) {
    return <div>処理中...</div>
  }

  if (!user) {
    return <div>ユーザーが見つかりませんでした。</div>
  }

  return <LeaveComplete/>;
};

export default Index;

frontend/src/app/mypage/page.tsx
import { NextPage } from 'next/types';
import Mypage from '@/components/pages/Mypage';
import useAuth from '@/hooks/useAuth';

const Index: NextPage = () => {
  const { loading, user } = useAuth();
  
  if (loading) {
    return <div>処理中...</div>
  }

  if (!user) {
    return <div>ユーザーが見つかりませんでした</div>
  }

  return <Mypage/>;
};

export default Index;

frontend/src/app/register/page.tsx
import { NextPage } from 'next/types';
import Register from '@/components/pages/Register';
import useAuth from '@/hooks/useAuth';

const Index: NextPage = () => {
  const { loading, user } = useAuth();
  
  if (loading) {
    return <div>処理中...</div>
  }

  if (!user) {
    return <div>ユーザーが見つかりませんでした</div>
  }

  return <Register/>;
};

export default Index;
frontend/src/app/report/page.tsx
import { NextPage } from 'next/types';
import Report from '@/components/pages/Report';
import useAuth from '@/hooks/useAuth';

const Index: NextPage = () => {
  const { loading, user } = useAuth();
  
  if (loading) {
    return <div>処理中...</div>
  }

  if (!user) {
    return <div>ユーザーが見つかりませんでした</div>
  }

  return <Report onSubmissionFail={() => {} }/>;
};

export default Index;

frontend/src/app/reset_password/page.tsx
import { NextPage } from 'next/types';
import ResetPassword from '@/components/pages/ResetPassword';
import useAuth from '@/hooks/useAuth';

const Index: NextPage = () => {
  const { loading, user } = useAuth();
  
  if (loading) {
    return <div>処理中...</div>
  }

  if (!user) {
    return <div>ユーザーが見つかりませんでした</div>
  }

  return <ResetPassword/>;
};

export default Index;
frontend/src/app/time_tracking/page.tsx
import { NextPage } from 'next/types';
import TimeTracking from '@/components/pages/TimeTracking';
import useAuth from '@/hooks/useAuth';

const Index: NextPage = () => {
  const { loading, user } = useAuth();
  
  if (loading) {
    return <div>処理中...</div>
  }

  if (!user) {
    return <div>ユーザーが見つかりませんでした</div>
  }

  return <TimeTracking/>;
};

export default Index;

では、作成した勤怠管理アプリを動かしてみます。

Dockerの起動

$ docker-compose up -d –build

そして、ブラウザで、http://localhost:3000 または、http://localhost:8080

と入力して動かしてみると、

新規メモ.jpeg

というページが、表示されます。

そして、「▲アプリを始める」というボタンを押すと、ログインページに移動します。

しかし、

新規メモ.jpeg

というエラーが出ているので、

現在、Next.jsやLaravelとのAPI通信ができるように、コードを変更している最中でございます。

進展があれば、随時、更新されるので、都度、確認の方お願いします。

もし、ここまでの記事で、この部分、こうした方がいいやアドバイスなどがあれば、ありがたいです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?