Skip to content

Phase 2: UI構築編

このPhaseの目標

  • コンポーネント設計の考え方を理解する
  • TypeScriptでPropsの型定義ができる
  • Tailwind CSSでスタイリングできる
  • モックデータを使ってカンバンボードUIを構築する

事前に理解しておくこと

コンポーネント設計の考え方

UIを再利用可能な部品(コンポーネント)に分割します。

作成するコンポーネント一覧

コンポーネント役割Props
TaskCard1つのタスクを表示task: Task
StatusColumnステータス列(TODO等)status, tasks
TaskBoardカンバンボード全体tasks

Step 1: 型定義を作成する

学ぶこと

TypeScriptで型を定義し、コード全体で再利用します。

やること

  • [ ] types/ディレクトリを作成
  • [ ] types/task.tsを作成

ポイント解説

ポイント 1: Union Type(|)で Status を3つの値のいずれかに限定

tsx
export type Status = "TODO" | "IN_PROGRESS" | "DONE";

ポイント 2: Task 型でタスクの構造を定義

tsx
export type Task = {
  id: string;
  title: string;
  description: string;
  status: Status;  // 上で定義したStatus型を使用
  createdAt: Date;
};

ポイント 3: export type で他のファイルから使用可能に

完全なコード(クリックで展開)
tsx
// ファイルパス: types/task.ts

export type Status = "TODO" | "IN_PROGRESS" | "DONE";

export type Task = {
  id: string;
  title: string;
  description: string;
  status: Status;
  createdAt: Date;
};

つまずきポイント

エラー例: "Cannot find module '@/types/task'"
原因: パスエイリアスの設定ミス、またはファイルが存在しない
解決法: tsconfig.jsonの "paths" に "@/*" が設定されているか確認

動作確認

ファイルを保存してエラーが出なければOK。


Step 2: モックデータを作成する

学ぶこと

開発中に使用するダミーデータを作成します。

やること

  • [ ] lib/ディレクトリを作成
  • [ ] lib/mock-data.tsを作成

ポイント解説

ポイント 1: 定義した Task 型をインポートして使用

tsx
import { Task } from "@/types/task";

ポイント 2: Task[] で配列の各要素が Task 型であることを保証

tsx
export const mockTasks: Task[] = [
  {
    id: "1",
    title: "Next.jsプロジェクトのセットアップ",
    // ... Task型に沿った構造
  },
  // ...
];

ポイント 3: モックデータの利点

  • DBなしで開発を進められる
  • 各ステータス(TODO, IN_PROGRESS, DONE)のタスクを用意しておくとUIの確認がしやすい
完全なコード(クリックで展開)
tsx
// ファイルパス: lib/mock-data.ts

import { Task } from "@/types/task";

export const mockTasks: Task[] = [
  {
    id: "1",
    title: "Next.jsプロジェクトのセットアップ",
    description: "create-next-appでプロジェクトを作成する",
    status: "DONE",
    createdAt: new Date("2024-01-01"),
  },
  {
    id: "2",
    title: "コンポーネント設計",
    description: "TaskCard, StatusColumnなどのコンポーネントを設計する",
    status: "IN_PROGRESS",
    createdAt: new Date("2024-01-02"),
  },
  {
    id: "3",
    title: "Prismaでデータベース構築",
    description: "SQLiteとPrismaを使ってDBを構築する",
    status: "TODO",
    createdAt: new Date("2024-01-03"),
  },
  {
    id: "4",
    title: "Server Actionsの実装",
    description: "CRUD操作をServer Actionsで実装する",
    status: "TODO",
    createdAt: new Date("2024-01-04"),
  },
  {
    id: "5",
    title: "Vercelにデプロイ",
    description: "完成したアプリをVercelにデプロイする",
    status: "TODO",
    createdAt: new Date("2024-01-05"),
  },
];

動作確認

インポートエラーが出なければOK。


Step 3: TaskCardコンポーネントを作成する

学ぶこと

最小単位のコンポーネント「タスクカード」を作成します。

やること

  • [ ] components/ディレクトリを作成(まだの場合)
  • [ ] components/TaskCard.tsxを作成

ポイント解説

ポイント 1: Props 型で親から受け取る値の型を定義

tsx
type Props = {
  task: Task;
};

ポイント 2: 分割代入で { task } を Props から取り出す

tsx
export default function TaskCard({ task }: Props) {
  // task.title, task.description などでアクセス
}

ポイント 3: Tailwind CSS でスタイリング

クラス効果
rounded-lg角丸
shadow-sm小さい影
hover:shadow-mdホバー時に影を大きく
transition-shadow影の変化をアニメーション
line-clamp-22行で切り捨て
完全なコード(クリックで展開)
tsx
// ファイルパス: components/TaskCard.tsx

import { Task } from "@/types/task";

type Props = {
  task: Task;
};

export default function TaskCard({ task }: Props) {
  return (
    <div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 hover:shadow-md transition-shadow">
      <h3 className="font-medium text-gray-800">{task.title}</h3>
      <p className="mt-2 text-sm text-gray-500 line-clamp-2">
        {task.description}
      </p>
      <div className="mt-3 text-xs text-gray-400">
        {task.createdAt.toLocaleDateString("ja-JP")}
      </div>
    </div>
  );
}

つまずきポイント

エラー例: "Type '{ task: Task; }' is not assignable to type 'IntrinsicAttributes'"
原因: Propsの型定義が間違っている
解決法: Props型を正しく定義し、コンポーネントの引数に適用
エラー例: line-clamp-2 が効かない
原因: Tailwind CSSのline-clampプラグインが必要(v3.3以降は標準)
解決法: Tailwindのバージョンを確認、または @tailwindcss/line-clamp をインストール

動作確認

次のStepでStatusColumnに組み込んでから確認します。


Step 4: StatusColumnコンポーネントを作成する

学ぶこと

ステータスごとのカラム(列)コンポーネントを作成します。

やること

  • [ ] components/StatusColumn.tsxを作成

ポイント解説

ポイント 1: 設定オブジェクトでステータスごとの色を管理

tsx
const statusConfig = {
  TODO: {
    label: "TODO",
    bgColor: "bg-amber-50",
    // ...
  },
  IN_PROGRESS: { /* ... */ },
  DONE: { /* ... */ },
};

ポイント 2: テンプレートリテラルで動的にクラスを適用

tsx
const config = statusConfig[status];
<div className={`${config.bgColor} ${config.borderColor} ...`}>

ポイント 3: .map() で配列をループしてコンポーネントを生成

tsx
{tasks.map((task) => (
  <TaskCard key={task.id} task={task} />
))}

ポイント 4: key 属性はReactがリストの各要素を識別するために必要

完全なコード(クリックで展開)
tsx
// ファイルパス: components/StatusColumn.tsx

import { Task, Status } from "@/types/task";
import TaskCard from "./TaskCard";

type Props = {
  status: Status;
  tasks: Task[];
};

const statusConfig = {
  TODO: {
    label: "TODO",
    bgColor: "bg-amber-50",
    borderColor: "border-amber-200",
    headerBg: "bg-amber-100",
    headerText: "text-amber-800",
  },
  IN_PROGRESS: {
    label: "IN PROGRESS",
    bgColor: "bg-blue-50",
    borderColor: "border-blue-200",
    headerBg: "bg-blue-100",
    headerText: "text-blue-800",
  },
  DONE: {
    label: "DONE",
    bgColor: "bg-green-50",
    borderColor: "border-green-200",
    headerBg: "bg-green-100",
    headerText: "text-green-800",
  },
};

export default function StatusColumn({ status, tasks }: Props) {
  const config = statusConfig[status];

  return (
    <div
      className={`${config.bgColor} ${config.borderColor} border rounded-lg p-4 min-h-[500px]`}
    >
      <div
        className={`${config.headerBg} ${config.headerText} px-3 py-2 rounded-md font-semibold mb-4 flex items-center justify-between`}
      >
        <span>{config.label}</span>
        <span className="text-sm font-normal">({tasks.length})</span>
      </div>
      <div className="space-y-3">
        {tasks.map((task) => (
          <TaskCard key={task.id} task={task} />
        ))}
      </div>
    </div>
  );
}

つまずきポイント

エラー例: "Each child in a list should have a unique 'key' prop"
原因: map()で生成した要素にkeyがない
解決法: <TaskCard key={task.id} ... /> のようにkeyを追加

動作確認

次のStepでTaskBoardに組み込んでから確認します。


Step 5: TaskBoardコンポーネントを作成する

学ぶこと

カンバンボード全体のコンポーネントを作成します。

やること

  • [ ] components/TaskBoard.tsxを作成

ポイント解説

ポイント 1: グリッドレイアウトで3列に分割

tsx
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">

ポイント 2: md:grid-cols-3 でレスポンシブ対応(中サイズ以上で3列、それ以下で1列)

ポイント 3: .filter() でステータスごとにタスクを絞り込み

tsx
tasks={tasks.filter((task) => task.status === status)}
完全なコード(クリックで展開)
tsx
// ファイルパス: components/TaskBoard.tsx

import { Task, Status } from "@/types/task";
import StatusColumn from "./StatusColumn";

type Props = {
  tasks: Task[];
};

const statuses: Status[] = ["TODO", "IN_PROGRESS", "DONE"];

export default function TaskBoard({ tasks }: Props) {
  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
      {statuses.map((status) => (
        <StatusColumn
          key={status}
          status={status}
          tasks={tasks.filter((task) => task.status === status)}
        />
      ))}
    </div>
  );
}

つまずきポイント

エラー例: カラムが縦に並んでしまう
原因: 画面幅が狭い(md以下)
解決法: ブラウザの幅を広げるか、grid-cols-3からmd:を削除

動作確認

次のStepでページに組み込んでから確認します。


Step 6: タスク一覧ページを更新する

学ぶこと

作成したコンポーネントをページに組み込みます。

やること

  • [ ] app/tasks/page.tsxを更新

変更箇所

Phase 1で作成したシンプルなページを、カンバンボードUIに更新します。

変更箇所 1: インポートを追加

tsx
import TaskBoard from "@/components/TaskBoard";
import { mockTasks } from "@/lib/mock-data";

変更箇所 2: ページ全体を書き換え

tsx
// Phase 1 のコード
export default function TasksPage() {
  return (
    <main className="p-8">
      <h1 className="text-2xl font-bold">タスク一覧</h1>
      <p className="mt-4 text-gray-600">ここにタスク一覧が表示されます</p>
    </main>
  );
}

// ↓ Phase 2 で更新

export default function TasksPage() {
  return (
    <main className="min-h-screen p-8">
      <div className="max-w-7xl mx-auto">
        <div className="flex items-center justify-between mb-8">
          <h1 className="text-3xl font-bold text-gray-800">タスク一覧</h1>
          <button className="...">+ 新規タスク</button>
        </div>
        <TaskBoard tasks={mockTasks} />
      </div>
    </main>
  );
}

ポイント解説

  • Server Component: "use client"がないのでサーバーで実行
  • データの流れ: mockTasksTaskBoardStatusColumnTaskCard
完全なコード(クリックで展開)
tsx
// ファイルパス: app/tasks/page.tsx

import TaskBoard from "@/components/TaskBoard";
import { mockTasks } from "@/lib/mock-data";

export default function TasksPage() {
  return (
    <main className="min-h-screen p-8">
      <div className="max-w-7xl mx-auto">
        <div className="flex items-center justify-between mb-8">
          <h1 className="text-3xl font-bold text-gray-800">タスク一覧</h1>
          <button className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors">
            + 新規タスク
          </button>
        </div>
        <TaskBoard tasks={mockTasks} />
      </div>
    </main>
  );
}

つまずきポイント

エラー例: "Module not found: Can't resolve '@/components/TaskBoard'"
原因: パスが間違っている、またはファイルが存在しない
解決法: ファイルが正しい場所に存在するか確認

動作確認

http://localhost:3000/tasks にアクセスして、3列のカンバンボードが表示されればOK。


Step 7: Counterコンポーネントを削除する

学ぶこと

Phase 1で作成したテスト用コンポーネントを整理します。

やること

  • [ ] components/Counter.tsxを削除(不要な場合)

Phase 1で作成したCounterコンポーネントはServer/Client Componentの理解のためのものだったので、不要であれば削除してください。


Phase 2 完了チェックリスト

  • [ ] types/task.tsで型定義を作成した
  • [ ] lib/mock-data.tsでモックデータを作成した
  • [ ] components/TaskCard.tsxを作成した
  • [ ] components/StatusColumn.tsxを作成した
  • [ ] components/TaskBoard.tsxを作成した
  • [ ] app/tasks/page.tsxにTaskBoardを組み込んだ
  • [ ] カンバンボードが3列で表示される
  • [ ] 各ステータスに対応するタスクが表示される

ここまでの成果

コンポーネント構成

現時点のディレクトリ構成

task-manager/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   ├── globals.css
│   └── tasks/
│       └── page.tsx        ← 更新
├── components/
│   ├── TaskCard.tsx        ← 新規
│   ├── StatusColumn.tsx    ← 新規
│   └── TaskBoard.tsx       ← 新規
├── lib/
│   └── mock-data.ts        ← 新規
└── types/
    └── task.ts             ← 新規

次のPhaseへ

Phase 3では、タスクの追加・削除機能を実装します。

  • useStateでの状態管理
  • フォームの作成
  • Server Actionsの基礎

Phase 3: データ操作編へ進む →