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.