feat(web): complete du-an project pages, neighborhood components, and public notification bell
- Add grid/map view toggle on /du-an listing page with Mapbox project markers - Enhance du-an detail with master plan viewer, neighborhood radar chart, POI map, and price history chart - Create neighborhood component suite: radar chart (Recharts), POI map (Mapbox), score badges - Add du-an API client, server-side fetching, and React Query hooks - Wire NotificationBell into public layout header for authenticated users - Fix missing PROJECT_STATUS_COLORS import in du-an detail client Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
58
apps/web/lib/du-an-server.ts
Normal file
58
apps/web/lib/du-an-server.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Server-side project data fetching for Next.js Server Components.
|
||||
*
|
||||
* Uses `fetch` directly (no browser-only helpers) so it can run
|
||||
* inside `generateMetadata`, `generateStaticParams`, `sitemap()`, etc.
|
||||
*/
|
||||
|
||||
import type { ProjectDetail, ProjectSummary, PaginatedResult } from './du-an-api';
|
||||
|
||||
const API_BASE_URL = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:3001/api/v1';
|
||||
|
||||
/**
|
||||
* Fetch a single project by slug — server-only.
|
||||
* Returns `null` when the project is not found (404) so callers can `notFound()`.
|
||||
*/
|
||||
export async function fetchProjectBySlug(slug: string): Promise<ProjectDetail | null> {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/projects/${slug}`, {
|
||||
next: { revalidate: 300 }, // ISR: re-validate every 5 min
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
return (await res.json()) as ProjectDetail;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch active projects — server-only, used by the dynamic sitemap.
|
||||
*/
|
||||
export async function fetchProjects(params: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
city?: string;
|
||||
status?: string;
|
||||
}): Promise<PaginatedResult<ProjectSummary>> {
|
||||
const query = new URLSearchParams({
|
||||
page: String(params.page ?? 1),
|
||||
limit: String(params.limit ?? 100),
|
||||
});
|
||||
if (params.city) query.append('city', params.city);
|
||||
if (params.status) query.append('status', params.status);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/projects?${query}`, {
|
||||
next: { revalidate: 3600 }, // re-validate every hour for sitemap
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return { data: [], total: 0, page: 1, limit: 100, totalPages: 0 };
|
||||
}
|
||||
|
||||
return (await res.json()) as PaginatedResult<ProjectSummary>;
|
||||
} catch {
|
||||
return { data: [], total: 0, page: 1, limit: 100, totalPages: 0 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user