Chuyển đến nội dung chính

レッスン 15: Vibe コーディング フルスタック アプリ — パート 2: フロントエンド

React + Next.js + TailwindCSS と Vibecoding を使用してフロントエンドを構築します。コンポーネントの生成、レスポンシブ レイアウト、フォーム処理、API 統合、状態管理。 AI を使用したフロントエンド コンポーネントのテスト。

💻 プログラミング — レッスン 15 レッスン 15: Vibe コーディング フルスタック アプリ — パート 2: フロントエンド

GitHub Copilot を使用した Vibe コーディング: 基本から高度まで

パート 5: Vibe コーディングの実際 — 実際のプロジェクトの構築

xdev.asia

1. フロントエンドプロジェクトのセットアップ

// Prompt cho Agent Mode:
Create a Next.js 15 frontend project with:
- TypeScript, App Router
- TailwindCSS v4 with custom design tokens
- Shadcn/ui components
- Axios for API calls
- React Hook Form + Zod validation
- Zustand for state management
- Connect to the backend API at localhost:3001

エージェントモードは自動的に実行されます npx create-next-app、依存関係をインストールし、TailwindCSS を構成し、プロジェクト構造を足場にします。

2. コンポーネントの生成 — Vibe コーディングの力

2.1.レイアウトコンポーネント

// Prompt:
Create a responsive dashboard layout with:
- Sidebar navigation (collapsible on mobile)
- Top header with user avatar dropdown
- Main content area with breadcrumbs
- Use TailwindCSS, dark mode support
- Sidebar items: Dashboard, Projects, Tasks, Settings

Copilot は、応答性の高いブレークポイントを含む完全なレイアウトを生成します。

// src/components/layout/DashboardLayout.tsx
'use client';

import { useState } from 'react';
import { Menu, X, ChevronDown } from 'lucide-react';
import Link from 'next/link';

interface DashboardLayoutProps {
  children: React.ReactNode;
}

export function DashboardLayout({ children }: DashboardLayoutProps) {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  const navItems = [
    { href: '/dashboard', label: 'Dashboard', icon: '📊' },
    { href: '/projects', label: 'Projects', icon: '📁' },
    { href: '/tasks', label: 'Tasks', icon: '✅' },
    { href: '/settings', label: 'Settings', icon: '⚙️' },
  ];

  return (
    <div className="flex h-screen bg-gray-50 dark:bg-gray-900">
      {/* Sidebar */}
      <aside className={`
        fixed inset-y-0 left-0 z-50 w-64 bg-white dark:bg-gray-800
        transform transition-transform duration-200 ease-in-out
        ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
        lg:translate-x-0 lg:static lg:inset-0
      `}>
        <nav className="mt-8 px-4 space-y-2">
          {navItems.map((item) => (
            <Link key={item.href} href={item.href}
              className="flex items-center px-4 py-3 rounded-lg
                hover:bg-gray-100 dark:hover:bg-gray-700">
              <span className="mr-3">{item.icon}</span>
              {item.label}
            </Link>
          ))}
        </nav>
      </aside>

      {/* Main content */}
      <main className="flex-1 overflow-y-auto p-6">
        {children}
      </main>
    </div>
  );
}

2.2.タスクボードコンポーネント

// Prompt:
Create a Kanban board component for tasks with:
- Columns: Todo, In Progress, In Review, Done
- Drag and drop between columns (use @dnd-kit)
- Task cards showing title, priority badge, assignee avatar, due date
- Add task button per column
- Animate card movement

3. AIによるフォームハンドリング

// Prompt:
Create a task creation form with:
- Title (required, max 255 chars)
- Description (optional, rich text editor)
- Priority select (Low, Medium, High, Urgent)
- Due date picker
- Assignee select (fetch from API)
- Project select
- Use React Hook Form + Zod validation
- Show inline validation errors
- Submit to POST /api/tasks
// src/components/tasks/CreateTaskForm.tsx
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { api } from '@/lib/api';

const taskSchema = z.object({
  title: z.string().min(1, 'Title is required').max(255),
  description: z.string().optional(),
  priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']),
  dueDate: z.string().optional(),
  assigneeId: z.string().uuid().optional(),
  projectId: z.string().uuid(),
});

type TaskFormData = z.infer<typeof taskSchema>;

export function CreateTaskForm({ projectId, onSuccess }: {
  projectId: string;
  onSuccess: () => void;
}) {
  const { register, handleSubmit, formState: { errors, isSubmitting } } =
    useForm<TaskFormData>({
      resolver: zodResolver(taskSchema),
      defaultValues: { projectId, priority: 'MEDIUM' },
    });

  const onSubmit = async (data: TaskFormData) => {
    await api.post('/tasks', data);
    onSuccess();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <input {...register('title')}
          placeholder="Task title"
          className="w-full px-4 py-2 border rounded-lg" />
        {errors.title &&
          <p className="text-red-500 text-sm mt-1">{errors.title.message}</p>}
      </div>

      <div>
        <select {...register('priority')} className="w-full px-4 py-2 border rounded-lg">
          <option value="LOW">🟢 Low</option>
          <option value="MEDIUM">🟡 Medium</option>
          <option value="HIGH">🟠 High</option>
          <option value="URGENT">🔴 Urgent</option>
        </select>
      </div>

      <button type="submit" disabled={isSubmitting}
        className="w-full bg-blue-600 text-white py-2 rounded-lg
          hover:bg-blue-700 disabled:opacity-50">
        {isSubmitting ? 'Creating...' : 'Create Task'}
      </button>
    </form>
  );
}

4. APIの統合

// Prompt:
Create an API client module with:
- Axios instance with base URL and auth interceptor
- Automatic token refresh on 401
- Type-safe API functions for tasks, projects, auth
- Loading and error state hooks
// src/lib/api.ts
import axios from 'axios';

export const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api',
});

// Auth interceptor
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('accessToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Auto refresh on 401
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      const refreshToken = localStorage.getItem('refreshToken');
      if (refreshToken) {
        const { data } = await axios.post(
          `${api.defaults.baseURL}/auth/refresh`,
          { refreshToken }
        );
        localStorage.setItem('accessToken', data.accessToken);
        error.config.headers.Authorization = `Bearer ${data.accessToken}`;
        return api.request(error.config);
      }
    }
    return Promise.reject(error);
  }
);

5. Zustandによる状態管理

// Prompt:
Create a Zustand store for task management with:
- Tasks list with CRUD operations
- Filter by status, priority, assignee
- Optimistic updates for status changes
- Integration with the API client
// src/stores/taskStore.ts
import { create } from 'zustand';
import { api } from '@/lib/api';

interface Task {
  id: string;
  title: string;
  status: 'TODO' | 'IN_PROGRESS' | 'IN_REVIEW' | 'DONE';
  priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
  assignee?: { id: string; name: string; avatar?: string };
}

interface TaskStore {
  tasks: Task[];
  loading: boolean;
  fetchTasks: (projectId: string) => Promise<void>;
  updateStatus: (taskId: string, status: Task['status']) => Promise<void>;
}

export const useTaskStore = create<TaskStore>((set, get) => ({
  tasks: [],
  loading: false,

  fetchTasks: async (projectId) => {
    set({ loading: true });
    const { data } = await api.get(`/tasks?projectId=${projectId}`);
    set({ tasks: data.data, loading: false });
  },

  updateStatus: async (taskId, status) => {
    // Optimistic update
    const previousTasks = get().tasks;
    set({
      tasks: previousTasks.map((t) =>
        t.id === taskId ? { ...t, status } : t
      ),
    });

    try {
      await api.put(`/tasks/${taskId}`, { status });
    } catch {
      // Rollback on error
      set({ tasks: previousTasks });
    }
  },
}));

6.レスポンシブデザイン

// Prompt:
Make the task board responsive:
- Desktop: 4-column Kanban layout
- Tablet: 2-column grid with horizontal scroll
- Mobile: single column with tab navigation for each status
- Preserve drag and drop on tablet, disable on mobile

Copilot は、画面サイズに基づいて応答性の高いブレークポイントと条件付きレンダリングを生成します。

7. ダークモード

// Prompt:
Add dark mode toggle with:
- System preference detection
- Manual toggle in header
- Persist preference in localStorage
- Smooth transition animation

8. フロントエンドコンポーネントのテスト

// Prompt:
Write tests for CreateTaskForm component:
- Renders all form fields
- Shows validation errors for empty title
- Submits form data to API
- Disables button while submitting
- Calls onSuccess after successful submit
Use React Testing Library + Jest
// src/components/tasks/__tests__/CreateTaskForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CreateTaskForm } from '../CreateTaskForm';
import { api } from '@/lib/api';

jest.mock('@/lib/api');

describe('CreateTaskForm', () => {
  const mockOnSuccess = jest.fn();
  const projectId = 'test-project-id';

  it('shows validation error for empty title', async () => {
    render(<CreateTaskForm projectId={projectId} onSuccess={mockOnSuccess} />);

    await userEvent.click(screen.getByText('Create Task'));

    await waitFor(() => {
      expect(screen.getByText('Title is required')).toBeInTheDocument();
    });
  });

  it('submits form data correctly', async () => {
    (api.post as jest.Mock).mockResolvedValue({ data: {} });

    render(<CreateTaskForm projectId={projectId} onSuccess={mockOnSuccess} />);

    await userEvent.type(screen.getByPlaceholderText('Task title'), 'New Task');
    await userEvent.click(screen.getByText('Create Task'));

    await waitFor(() => {
      expect(api.post).toHaveBeenCalledWith('/tasks', expect.objectContaining({
        title: 'New Task',
        projectId,
        priority: 'MEDIUM',
      }));
      expect(mockOnSuccess).toHaveBeenCalled();
    });
  });
});

9. Copilot を使用したフロントエンドのデバッグ

実行時エラーが発生した場合:

// Copy error vào chat:
I'm getting this error in the browser console:
"TypeError: Cannot read properties of undefined (reading 'map')"
at TaskBoard.tsx:45

The tasks data from the API might be null on first render. Fix this with proper null checks and loading state.

Copilot はエラーを分析し、根本原因を見つけてコードを修正します。多くの場合、Null チェックを追加したり状態をロードしたりします。

10. まとめ

タスク バイブコーディングプロンプト 出力
レイアウト レイアウト構造の説明 レスポンシブなダッシュボード
コンポーネント UIの動作を説明する インタラクティブなコンポーネント
フォーム フィールドと検証の説明 タイプセーフなフォーム
API エンドポイントの説明 タイプセーフなクライアント
州 データフローの説明 ズスタンド店
テスト 「X のテストを作成する」 RTLテストスイート

フロントエンド Vibe コーディングは、UI コンポーネントが非常に強力であるため、 パターンが何度も繰り返される — AI は反復的な UI コードの生成に優れています。

次の記事: に展開します モバイルとクロスプラットフォーム — ネイティブに反応し、Vibe コーディングでフラッターします。