- Add `formatCompact` as an exported alias for `formatPrice` in lib/currency.ts
- Replace 5 inline copies of the tỷ/triệu compact formatter:
- components/map/listing-map.tsx (local `formatPrice` fn)
- components/agents/agent-profile-client.tsx (local `fmtVND` fn)
- app/(dashboard)/dashboard/saved-searches/page.tsx (local `formatPrice` fn)
- app/(public)/page.tsx (local `formatVnd` fn + `vndFmt` Intl instance)
- components/listings/price-history-chart.tsx (local `formatMillions` + `priceToMillions`)
All call sites now import from the canonical lib/currency module.
PriceHistoryChart now stores raw VND in chart data (was: millions) so
formatCompact emits correct tỷ/triệu labels using canonical thresholds.
Pre-existing test failures in inquiry/lead/AVM specs are unrelated to this change.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Replace 200+ individual mapboxgl.Marker DOM nodes with a single GeoJSON
source using Mapbox built-in clustering (clusterRadius=50, maxZoom=14)
- Cluster + unclustered price labels render as WebGL symbol/circle layers —
zero per-frame DOM cost, 60fps pan on mid-range Android
- Decouple selectedListingId updates from full marker teardown: selection
state is now a `selected:0|1` feature property, updated via setData() only
- fitBounds no longer fires on hover/selection changes — camera moves only
when the listings array identity changes (filter change)
- Fix stale onMarkerClick closure with a stable ref pattern
- Decided clustering strategy: Mapbox built-in over supercluster (no extra
dep, sufficient for <5k results; see docs/perf/listing-map-perf-analysis.md)
- Add perf analysis doc to apps/web/docs/perf/
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Mapbox theming
--------------
- New hook `lib/mapbox-style.ts` returning streets-v12 (light) or
dark-v11 (dark) from the app's useTheme().
- Six map components now initialise with the themed style and
`map.setStyle(...)` on theme change: project-map, park-map,
listing-map, district-heatmap (plus re-adding its heatmap source
after style.load), neighborhood-poi-map, valuation/comparables-map.
- Marker / popup DOM styles swapped from hard-coded white/#666/#green
to shadcn CSS tokens (--card, --card-foreground, --muted-foreground,
--primary, --border). Global Mapbox popup + control + attribution
skins added in app/globals.css.
- POI filter pills on neighborhood-poi-map were hard-coded `bg-white`
which rendered same-colour text on white in dark mode — switched to
`bg-card`/`bg-card/60` for proper contrast.
- Extend the MockMap in comparables-map.spec.tsx with setStyle/on
so the new theme-sync effect doesn't blow up in tests.
Detail client normaliser (du-an-server)
---------------------------------------
- Project media from the backend is a `string[]` (raw URLs) or richer
`{url,...}` objects. Handle both shapes and drop entries without
a URL so we never feed "" to <Image src>.
- Amenities are `string[]` in the DB but the frontend type expects
`{id,name,icon,category}`; normalise strings into objects so the
AmenitiesTab has stable keys and a displayable name.
Resolves three classes of runtime warnings on /du-an/<slug>:
"Image is missing required 'src' property", "ReactDOM.preload ...
empty href", and "Each child in a list should have a unique 'key'
prop" (AmenitiesTab).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rewrite prisma/seed.ts to populate all 27 models with realistic
Vietnamese real estate data (8 users with login, 10 properties,
10 listings, orders, payments, reviews, notifications, etc.)
- Replace all emoji icons with Lucide React SVG icons across frontend
for consistent rendering, sizing, and accessibility
- Redesign dashboard nav: grouped sidebar with section headers,
primary/secondary split on desktop, icon-only secondary items
- Replace language switcher flag emoji with Globe icon
- Replace SVG theme toggle with Lucide Moon/Sun icons
- Fix API startup: graceful fallback for Sentry profiling, Google OAuth,
and Zalo OAuth when credentials are not configured
- Relax rate limiting in development mode (10k req/min)
- Fix listings API to include media[] array in search response
- Add optional chaining for property.media across frontend components
- Update OAuth strategy tests to match graceful fallback behavior
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Add global QueryErrorResetBoundary wrapping the app so TanStack Query
errors are caught with a retry UI instead of crashing. Enable
throwOnError in QueryClient defaults. Update ListingMap to use real
latitude/longitude from API when available, falling back to city-based
jitter for listings without coordinates.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Replace innerHTML/setHTML with DOM API (createElement/textContent/setDOMContent)
to prevent XSS via user-controlled listing titles, URLs, and prices
- Add Content-Security-Policy header to next.config.js with proper directives
for Mapbox, API, images, workers, and frame-ancestors
- Add X-CSRF-Token header to media upload fetch call, matching apiClient behavior
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Create (public) route group with landing page (hero, featured listings, district links, stats, CTA)
- Create search page with filter sidebar, list/map/split view modes, URL-synced filters, pagination
- Build ListingMap component with CSS-based marker visualization and popup details
- Build FilterBar with transaction type, property type, city, price range, area, bedrooms filters
- Build PropertyCard and SearchResults components with responsive grid layout
- Update middleware to allow public access to / and /search routes
- Move dashboard home to /dashboard to avoid route conflict
- All content in Vietnamese, mobile responsive
Co-Authored-By: Paperclip <noreply@paperclip.ing>