# 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({ page: 1 }); const [viewMode, setViewMode] = useState<'grid' | 'list' | 'map'>('grid'); const { data, isLoading } = useProjectsSearch(filters); return (
{viewMode === 'map' ? ( ) : viewMode === 'list' ? ( data?.data.map(p => ) ) : (
{data?.data.map(p => )}
)}
); } ``` ### 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 { 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 ; } ``` ### 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 (
{data?.data.map(p => (

{p.name}

))}
); } ``` --- ## πŸ“š 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(`/projects?...`), getBySlug: (slug) => apiClient.get(`/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 Title ``` βœ… Tailwind classes for layout ```tsx
``` βœ… 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'; ``` βœ… Images with next/image ```tsx {project.name} ``` βœ… 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'; ``` --- ## πŸ” 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 ; ``` --- ## πŸ“Š 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(); expect(screen.getByText(mockProject.name)).toBeInTheDocument(); }); }); ``` --- ## 🚨 Common Mistakes to Avoid ❌ **Don't** use `` tags β†’ use `Link` from `@/i18n/navigation` ```tsx // ❌ Wrong Project // βœ… Right import { Link } from '@/i18n/navigation'; Project ``` ❌ **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 ; } ``` ❌ **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';

{formatPrice('1000000')}

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