# Next.js Frontend - Quick Reference

## 🗂️ File Organization

```
apps/web/
├── app/[locale]/                    # Pages
│   ├── (public)/du-an/              # Public project browsing
│   ├── (dashboard)/projects/        # Developer project management
│   └── layout.tsx                   # Root layout
├── components/                      # UI components
│   ├── ui/                          # Primitives (button, card, input...)
│   ├── du-an/                       # Project-specific components
│   └── map/                         # Mapbox integrations
├── lib/
│   ├── api-client.ts                # Fetch wrapper (CSRF, refresh)
│   ├── du-an-api.ts                 # Project API methods
│   ├── du-an-server.ts              # Server-side fetch for metadata
│   ├── hooks/use-du-an.ts           # React Query hooks
│   └── utils.ts                     # Helpers (cn, formatPrice, etc.)
└── tailwind.config.ts               # Design tokens
```

---

## 🚀 Common Patterns

### 1. Public Browse Page with Filters & Map

```tsx
// pages/du-an/page.tsx
'use client';

export default function DuAnPage() {
  const [filters, setFilters] = useState<SearchProjectsParams>({ page: 1 });
  const [viewMode, setViewMode] = useState<'grid' | 'list' | 'map'>('grid');
  const { data, isLoading } = useProjectsSearch(filters);

  return (
    <div className="mx-auto max-w-7xl px-4 py-6">
      <ProjectFilterBar filters={filters} onFilterChange={setFilters} />
      {viewMode === 'map' ? (
        <ProjectMap projects={data?.data || []} />
      ) : viewMode === 'list' ? (
        data?.data.map(p => <ProjectListItem key={p.id} project={p} />)
      ) : (
        <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {data?.data.map(p => <ProjectCard key={p.id} project={p} />)}
        </div>
      )}
    </div>
  );
}
```

### 2. Detail Page with Server-Side Rendering

```tsx
// pages/du-an/[slug]/page.tsx
import type { Metadata } from 'next';

export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = await params;
  const project = await fetchProjectBySlug(slug);
  if (!project) return { title: 'Not found' };
  
  return {
    title: `${project.name} — ${project.developer.name}`,
    description: project.description?.slice(0, 160),
  };
}

export default async function Page({ params }) {
  const { slug } = await params;
  const project = await fetchProjectBySlug(slug);
  if (!project) notFound();
  
  return <DuAnDetailClient project={project} />;
}
```

### 3. Admin CRUD Page

```tsx
// pages/projects/page.tsx
'use client';

export default function ProjectsPage() {
  const isDeveloper = useAuthStore(s => s.user?.role === 'DEVELOPER');
  
  const { data } = useQuery({
    queryKey: ['projects', isDeveloper],
    queryFn: () => isDeveloper ? duAnApi.searchMine() : duAnApi.search(),
  });
  
  const deleteMutation = useMutation({
    mutationFn: duAnApi.delete,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['projects'] }),
  });

  return (
    <div>
      <Link href="/projects/new">
        <Button><Plus /> Thêm dự án</Button>
      </Link>
      {data?.data.map(p => (
        <div key={p.id}>
          <h3>{p.name}</h3>
          <Link href={`/projects/${p.id}/edit`}><Pencil /></Link>
          <Button onClick={() => deleteMutation.mutate(p.id)}>
            <Trash2 />
          </Button>
        </div>
      ))}
    </div>
  );
}
```

---

## 📚 Key Files to Reference

| Task | File | Notes |
|------|------|-------|
| Browse projects | `app/[locale]/(public)/du-an/page.tsx` | Search + filter + map |
| Project detail | `app/[locale]/(public)/du-an/[slug]/page.tsx` | Server-side + tabs |
| Manage projects | `app/[locale]/(dashboard)/projects/page.tsx` | CRUD + React Query |
| Project card UI | `components/du-an/project-card.tsx` | Reusable component |
| Map integration | `components/du-an/project-map.tsx` | Mapbox with markers |
| Filter bar | `components/du-an/project-filter-bar.tsx` | Search + select filters |
| API methods | `lib/du-an-api.ts` | All endpoints |
| React Query hooks | `lib/hooks/use-du-an.ts` | Prefilled query keys |
| Server fetch | `lib/du-an-server.ts` | For metadata + static generation |
| Fetch wrapper | `lib/api-client.ts` | CSRF + auto-refresh |
| UI components | `components/ui/` | Button, Card, Input, Badge, etc. |
| Styling | `tailwind.config.ts` | Design tokens (colors, spacing, etc.) |

---

## 🔌 API Integration Checklist

✅ Define types in `du-an-api.ts`
```tsx
interface ProjectSummary { id, name, slug, ... }
interface ProjectDetail extends ProjectSummary { media, amenities, ... }
```

✅ Create API methods in `du-an-api.ts`
```tsx
export const duAnApi = {
  search: (params) => apiClient.get<PaginatedResult>(`/projects?...`),
  getBySlug: (slug) => apiClient.get<ProjectDetail>(`/projects/${slug}`),
};
```

✅ Create React Query hooks in `lib/hooks/use-du-an.ts`
```tsx
export function useProjectsSearch(params) {
  return useQuery({
    queryKey: projectKeys.search(params),
    queryFn: () => duAnApi.search(params),
  });
}
```

✅ Use in client components
```tsx
'use client';
const { data, isLoading } = useProjectsSearch(filters);
```

✅ Server-side fetching for metadata
```tsx
import { fetchProjectBySlug } from '@/lib/du-an-server';
export async function generateMetadata({ params }) {
  const project = await fetchProjectBySlug(slug);
  return { title: project.name };
}
```

---

## 🎨 UI & Styling Checklist

✅ Use shadcn/ui components
```tsx
<Button variant="outline" size="sm">Click me</Button>
<Card><CardHeader><CardTitle>Title</CardTitle></CardHeader></Card>
<Input placeholder="Search..." />
```

✅ Tailwind classes for layout
```tsx
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 mx-auto max-w-7xl px-4">
```

✅ Design tokens for colors
```tsx
className="text-primary"           // primary color
className="bg-muted text-muted-foreground"  // muted background
className="border border-border"   // borders
```

✅ Icons from lucide-react
```tsx
import { Building2, MapPin, Plus, Trash2 } from 'lucide-react';
<MapPin className="h-4 w-4" />
```

✅ Images with next/image
```tsx
<Image
  src={project.thumbnailUrl}
  alt={project.name}
  fill
  className="object-cover"
  sizes="(max-width: 768px) 100vw, 33vw"
/>
```

✅ Responsive utilities
```tsx
className="grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"
className="text-sm md:text-base lg:text-lg"
className="px-4 sm:px-6 lg:px-8"
```

---

## 🗺️ Mapbox Setup

```tsx
// .env.local
NEXT_PUBLIC_MAPBOX_TOKEN=pk_...

// Import in component
'use client';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';

// Use existing component
import { ProjectMap } from '@/components/du-an/project-map';
<ProjectMap projects={projects} />
```

---

## 🔐 Authentication

```tsx
// Check role in client component
'use client';
import { useAuthStore } from '@/lib/auth-store';

const role = useAuthStore(s => s.user?.role);
if (role !== 'DEVELOPER') return <NotAuthorized />;
```

---

## 📊 React Query Patterns

```tsx
// Query
const { data, isLoading, isError } = useQuery({
  queryKey: ['projects', filters],
  queryFn: () => duAnApi.search(filters),
  staleTime: 30_000,
});

// Mutation
const mutation = useMutation({
  mutationFn: (projectId) => duAnApi.delete(projectId),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['projects'] });
    toast.success('Deleted!');
  },
  onError: (error) => {
    toast.error(error.message);
  },
});

mutation.mutate(projectId);
```

---

## 🧪 Testing

```tsx
// Mirror the directory structure
components/du-an/project-card.tsx
components/du-an/__tests__/project-card.spec.tsx

// Test file
import { ProjectCard } from '../project-card';
import { render, screen } from '@testing-library/react';

describe('ProjectCard', () => {
  it('renders project name', () => {
    render(<ProjectCard project={mockProject} />);
    expect(screen.getByText(mockProject.name)).toBeInTheDocument();
  });
});
```

---

## 🚨 Common Mistakes to Avoid

❌ **Don't** use `<a>` tags → use `Link` from `@/i18n/navigation`
```tsx
// ❌ Wrong
<a href="/du-an/123">Project</a>

// ✅ Right
import { Link } from '@/i18n/navigation';
<Link href="/du-an/123">Project</Link>
```

❌ **Don't** fetch API inside `generateMetadata` without normalization
```tsx
// ❌ Wrong - might crash if fields missing
const project = await fetch(...);
return { title: project.name }; // Error if project is null

// ✅ Right - normalize first
const project = normalizeProjectDetail(raw);
if (!project) return { title: 'Not found' };
return { title: project.name };
```

❌ **Don't** mix Server Components and Client Components incorrectly
```tsx
// ❌ Wrong - can't use hooks in server component
export default async function Page() {
  const data = useQuery(...); // ERROR!
}

// ✅ Right - server fetches, client interacts
export default async function Page() {
  const data = await fetch(...);
  return <ClientComponent data={data} />;
}
```

❌ **Don't** forget to add `enabled` to conditional queries
```tsx
// ❌ Wrong - fires even when slug is empty
useQuery({
  queryFn: () => duAnApi.getBySlug(slug),
});

// ✅ Right
useQuery({
  queryFn: () => duAnApi.getBySlug(slug),
  enabled: !!slug,
});
```

---

## 💡 Pro Tips

1. **Use `cn()` for conditional classes**
   ```tsx
   className={cn(
     'default-class',
     isActive && 'active-class',
     variant === 'outline' && 'outline-class'
   )}
   ```

2. **Dynamic imports for heavy components**
   ```tsx
   const ProjectMap = dynamic(
     () => import('@/components/du-an/project-map').then(m => m.ProjectMap),
     { ssr: false }
   );
   ```

3. **Format numbers using `formatPrice()` utility**
   ```tsx
   import { formatPrice } from '@/lib/currency';
   <p>{formatPrice('1000000')}</p> // "1.000.000 ₫"
   ```

4. **Use query keys factory pattern for invalidation**
   ```tsx
   export const projectKeys = {
     all: ['projects'] as const,
     search: (params) => ['projects', 'search', params] as const,
   };
   // Then invalidate specific queries
   queryClient.invalidateQueries({ 
     queryKey: projectKeys.search(oldFilters) 
   });
   ```

5. **Leverage ISR for static pages**
   ```tsx
   export const revalidate = 3600; // Revalidate every hour
   ```
