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

Bài 15: Vibe Coding Full-Stack App — Phần 2: Frontend

Xây dựng frontend với React + Next.js + TailwindCSS bằng Vibe Coding. Component generation, responsive layout, form handling, API integration, state management. Testing frontend components với AI.

💻 Lập trình — Bài 15 Bài 15: Vibe Coding Full-Stack App — Phần 2: Frontend

Vibe Coding với GitHub Copilot: Từ Cơ bản đến Nâng cao

Phần 5: Vibe Coding thực chiến — Xây dựng dự án thực tế

xdev.asia

1. Setup Frontend Project

// 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

Agent Mode tự động chạy npx create-next-app, cài dependencies, cấu hình TailwindCSS, và scaffold project structure.

2. Component Generation — Sức mạnh của Vibe Coding

2.1. Layout Components

// 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 generate đầy đủ layout với responsive breakpoints:

// 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. Task Board Component

// 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. Form Handling với 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 Integration

// 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. State Management với 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. Responsive Design

// 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 generates responsive breakpoints và conditional rendering dựa trên screen size.

7. Dark Mode

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

8. Testing Frontend Components

// 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. Debug Frontend với Copilot

Khi gặp lỗi runtime:

// 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 phân tích error, tìm root cause, và fix code — thường thêm null checks và loading state.

10. Tổng kết

Task Vibe Coding Prompt Output
Layout Describe layout structure Responsive dashboard
Components Describe UI behavior Interactive components
Forms Describe fields + validation Type-safe forms
API Describe endpoints Type-safe client
State Describe data flow Zustand store
Tests "Write tests for X" RTL test suite

Frontend Vibe Coding rất mạnh vì UI components có pattern lặp lại nhiều — AI excel ở việc generate repetitive UI code.

Bài tiếp theo: Mở rộng sang Mobile & Cross-Platform — React Native và Flutter với Vibe Coding.