2024-3I プログラミング3 第13回 講義資料

2025年01月30日(木)1・2時限

1 連絡・概要

1.1 課題3

詳細は、前回の講義資料を確認してください。

1.1.1 開発例

高専テクノゼミ 実践教育プログラム 2025春 「Web開発」から抜粋。

1.2 高専テクノゼミ

高専テクノゼミ「実践教育プログラム2026春」のエントリが開始されています。

高専生や大学生が長期休暇に取り組む実践型の教育イベントです。今回のプログラムでは、AIを活用したアプリ開発を学びながら学生が各自で設定したテーマに自由に取り組みます。


高専テクノゼミ 「進路選択の部屋 ~高専からの進路 -大学編入と就職活動の対策-」

2 補足

本日は「課題3」に取り組んでください。以下、オプション的な内容 (課題3に必須の要素ではない) ですが Next.js 15 + Prisma 7 では検証できてきません。多少の読み替えで動作するはずです。すみません🙇

2.1 schema.prisma のシンタックスハイライト

VSCode の拡張機能として Prisma (識別子 prisma.prisma) をインストールすると schema.prisma がシンタックスハイライトされるようになります。

img

3 トースト通知機能

react-hot-toastというライブラリを使用して トースト通知 の機能を実装したサンプルを紹介します。

img

3.1 実装の手順

VSCodeのターミナル (Ctrl+J) から以下のコマンドを実行してライブラリをインストールします。

npm i react-hot-toast

サンプルコードを以下に示します。

"use client";
import toast, { Toaster } from "react-hot-toast";
import { twMerge } from "tailwind-merge";

const Page: React.FC = () => {
  const buttonStyle = twMerge(
    "rounded-md  px-3 py-1 ",
    "font-bold text-white",
    "bg-indigo-500 hover:bg-indigo-700"
  );

  const successNotify = (msg: string) => toast.success(msg);
  const errorNotify = (msg: string) => toast.error(msg);

  return (
    <main>
      <div className="mb-2 text-2xl font-bold ">トースト通知</div>
      <div className="flex gap-x-3">
        <button
          className={buttonStyle}
          onClick={() => successNotify("成功スタイルのトースト通知")}
        >
          success
        </button>
        <button
          className={buttonStyle}
          onClick={() => errorNotify("エラースタイルのトースト通知")}
        >
          error
        </button>
      </div>

      <Toaster position="top-center" />
    </main>
  );
};

export default Page;

トーストの「表示位置」や「外観 (アイコンや背景色)」、表示されてから自動消去されるまでの時間 などもカスタマイズ可能です。詳細については公式ドキュメントを参照してください。

4 モーダル表示

react-modalというライブラリを使用して モーダルウィンドウ を実装したサンプルを紹介します。

img

4.1 実装の手順

VSCodeのターミナルからライブラリをインストールします。react-modal は TypeScriptの型定義が同梱されていないため、別途 @types パッケージとして提供されている型定義ファイル をインストールする必要があります(型定義ファイルは開発時のみ必要なため、devDependencies として -D オプションを付けてインストールします)。

npm i react-modal
npm i -D @types/react-modal

まずは、モーダルウィンドウの外観を定義するコンポーネントを実装します。

"use client";

import React, { ReactNode } from "react";
import ReactModal from "react-modal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/free-solid-svg-icons";

interface Props {
  isOpen: boolean;
  onClose: () => void;
  children: ReactNode;
}

export const Modal: React.FC<Props> = (props) => {
  const { isOpen, onClose, children } = props;
  return (
    <ReactModal
      isOpen={isOpen}
      onRequestClose={onClose}
      contentLabel="Modal"
      closeTimeoutMS={300}
      ariaHideApp={false}
      className="relative z-50 h-screen w-screen bg-black/50"
      overlayClassName="fixed inset-0 bg-black_main flex items-center justify-center z-50"
    >
      <div className="flex size-full flex-col items-center justify-center">
        <div className="flex w-full justify-end md:w-[640px]">
          <button className="px-3 py-0.5  md:px-1" onClick={onClose}>
            <FontAwesomeIcon
              className="cursor-pointer text-2xl text-white hover:text-gray-300"
              icon={faXmark}
            />
          </button>
        </div>
        <div className="w-full bg-white p-3 md:w-[640px] md:rounded-md">
          {children}
        </div>
      </div>
    </ReactModal>
  );
};

つづいて、上記で定義した Modal コンポーネントを呼び出します。

"use client";
import { useState } from "react";
import { twMerge } from "tailwind-merge";
import { Modal } from "./_components/Modal"; // コンポーネントのインポート

const Page: React.FC = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <main>
      <div className="mb-2 text-2xl font-bold">モーダル</div>
      <div className="flex gap-x-3">
        <button
          className={twMerge(
            "rounded-md  px-3 py-1",
            "font-bold text-white",
            "bg-indigo-500 hover:bg-indigo-700"
          )}
          onClick={() => setIsModalOpen(true)}
        >
          モーダルウィンドウを開く
        </button>
      </div>

      <Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
        {/* モーダル内に表示するコンテンツを <Modal>...</Modal> の子要素として与える */}
        <div>
          <div className="text-xl font-bold">徒然草</div>
          <div>
            <p>
              つれづれなるままに、日暮らし硯に向かひて、心にうつりゆくよしなしごとを、
              そこはかとなく書きつくれば、あやしうこそものぐるほしけれ。
            </p>
          </div>
        </div>
      </Modal>
    </main>
  );
};

export default Page;

(プロンプト例)

TypeScipt 開発をしていると、ライブラリ本体にくわえて、ときどき npm i @types/xxxxx が要求されますが、この @types/ って、どのような位置づけのライブラリなのですか。

5 shadcn/ui

shadcn/uiという UIコンポーネントコレクション を利用するサンプルを示します。以下に示すように「スイッチ」や「カレンダ」「アコーディオン」「ドロップダウンメニュー」など 統一感のある各種UIコンポーネントを提供 してくれます。

img

利用可能なコンポーネントの一覧はこちらから確認することができます。

5.1 実装の手順

shadcn/ui のセットアップは一般的なライブラリとはやや異なります。まずは、VSCodeのターミナルから、以下のコマンドを実行してください。

npx shadcn@latest init

いくつかの質問が表示されるので、以下のように回答します。

Which style would you like to use? » New York
Which color would you like to use as the base color? » Slate
Would you like to use CSS variables for theming? … yes

(実行例)

Need to install the following packages:
shadcn@2.3.0
Ok to proceed? (y) y

✔ Preflight checks.
✔ Verifying framework. Found Next.js.
✔ Validating Tailwind CSS.
✔ Validating import alias.
√ Which style would you like to use? » New York
√ Which color would you like to use as the base color? » Slate
√ Would you like to use CSS variables for theming? ... yes
✔ Writing components.json.
✔ Checking registry.
✔ Updating tailwind.config.ts
✔ Updating src\app\globals.css
✔ Installing dependencies.
✔ Created 1 file:
  - src\lib\utils.ts

Success! Project initialization completed.
You may now add components.

つづいて、必要なUIを選んでインストールします。例えばスイッチUIを追加する場合は、以下のようにコマンドを実行します。

npx shadcn@latest add switch

また、カレンダUIを追加する場合は、以下のようにコマンドを実行します。

npx shadcn@latest add calendar

上記によってインストールされたライブラリは /src/components/ui/ にインストールされます。

実装例を以下に示します。

img

サンプルコードを以下に示します。

"use client";
import { useState } from "react";
import dayjs from "dayjs";
import { Switch } from "@/components/ui/switch";
import { Calendar } from "@/components/ui/calendar";

const Page: React.FC = () => {
  // スイッチ関連
  const [isDebugMode, setIsDebugMode] = useState(false);
  const handleIsDebugModeChange = (checked: boolean) => {
    if (checked) {
      console.log("Debug mode is enabled.");
    } else {
      console.log("Debug mode is disabled.");
    }
    setIsDebugMode(checked);
  };

  // カレンダ関連
  const dtFmt = "YYYY年MM月DD日が選択されました。";
  const [date, setDate] = useState<Date | undefined>();
  const [msg, setMsg] = useState<string>("日付を選択してください。");

  const handleDateChange = (date: Date | undefined) => {
    if (date === undefined) {
      setMsg("日付を選択してください。");
      return;
    }
    setMsg(dayjs(date).format(dtFmt));
    setDate(date);
  };

  return (
    <main>
      <div className="mb-4 text-2xl font-bold">shadcn/ui</div>

      <div className="flex flex-col gap-y-4">
        <div className="flex items-center gap-x-3">
          <Switch
            id="debug-mode"
            checked={isDebugMode}
            onCheckedChange={handleIsDebugModeChange}
          />
          <label htmlFor="debug-mode" className="cursor-pointer">
            デバッグモード
          </label>
        </div>

        <div className="">
          <Calendar
            mode="single"
            selected={date}
            onSelect={handleDateChange}
            className="inline-block rounded-md border"
          />
        </div>
        <div>{msg}</div>
      </div>
    </main>
  );
};

export default Page;

6 Lucide React Icon

shadcn/ui をインストールすると、それに関連する関係で lucide-react という「アイコンセットを提供するライブラリ」もインストールされます。このライブラリはアイコンは、既に紹介済みのFontAwesomeと併用することも可能です。

img

Lucide React が提供するアイコンの一覧 (2025/01/29時点で1548個) はこちらから検索・確認できます。

以下に、実装例を示します。

"use client";
import { Camera, ThumbsUp, Squirrel, Settings } from "lucide-react";

const Page: React.FC = () => {
  return (
    <main>
      <div className="mb-4 text-2xl font-bold">Lucide React Icon</div>

      <div className="flex gap-4">
        <div className="flex flex-col items-center">
          <div className="rounded-md border-2 p-2">
            <Camera className="size-24 text-red-500" />
          </div>
          <div>Camera</div>
        </div>
        <div className="flex flex-col items-center">
          <div className="rounded-md border-2 p-2">
            <ThumbsUp className="size-24 text-red-500" />
          </div>
          <div>ThumbsUp</div>
        </div>
        <div className="flex flex-col items-center">
          <div className="rounded-md border-2 p-2">
            <Squirrel className="size-24 text-red-500" />
          </div>
          <div>Squirrel</div>
        </div>
        <div className="flex flex-col items-center">
          <div className="rounded-md border-2 p-2">
            <Settings className="size-24 text-red-500" />
          </div>
          <div>Settings</div>
        </div>
      </div>
    </main>
  );
};

export default Page;