diff --git a/apps/web/components/listings/image-gallery.tsx b/apps/web/components/listings/image-gallery.tsx
index e4799c9..fb1627b 100644
--- a/apps/web/components/listings/image-gallery.tsx
+++ b/apps/web/components/listings/image-gallery.tsx
@@ -2,6 +2,8 @@
import Image from 'next/image';
import * as React from 'react';
+import { ImageLightbox } from '@/components/listings/image-lightbox';
+import { shimmerBlurDataURL } from '@/lib/image-blur';
import type { PropertyMedia } from '@/lib/listings-api';
import { cn } from '@/lib/utils';
@@ -13,6 +15,12 @@ interface ImageGalleryProps {
export function ImageGallery({ media, className }: ImageGalleryProps) {
const images = media.filter((m) => m.type === 'image').sort((a, b) => a.order - b.order);
const [selectedIndex, setSelectedIndex] = React.useState(0);
+ const [lightboxOpen, setLightboxOpen] = React.useState(false);
+
+ const openLightbox = React.useCallback((index: number) => {
+ setSelectedIndex(index);
+ setLightboxOpen(true);
+ }, []);
if (images.length === 0) {
return (
@@ -31,35 +39,58 @@ export function ImageGallery({ media, className }: ImageGalleryProps) {
{/* Main image */}
+
)}
+
+ {/* Lightbox */}
+
setLightboxOpen(false)}
+ />
);
}
diff --git a/apps/web/components/listings/image-upload.tsx b/apps/web/components/listings/image-upload.tsx
index b5cda51..abe1e09 100644
--- a/apps/web/components/listings/image-upload.tsx
+++ b/apps/web/components/listings/image-upload.tsx
@@ -84,12 +84,21 @@ export function ImageUpload({ images, onChange, maxFiles = 20, className }: Imag
return (
inputRef.current?.click()}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ inputRef.current?.click();
+ }
+ }}
+ aria-label="Tải ảnh lên. Kéo thả ảnh vào đây hoặc nhấn để chọn"
className={cn(
- 'flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 transition-colors',
+ 'flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
isDragging
? 'border-primary bg-primary/5'
: 'border-muted-foreground/25 hover:border-primary/50',
diff --git a/apps/web/components/map/listing-map.tsx b/apps/web/components/map/listing-map.tsx
index b3b93db..defaf2f 100644
--- a/apps/web/components/map/listing-map.tsx
+++ b/apps/web/components/map/listing-map.tsx
@@ -198,7 +198,7 @@ export function ListingMap({ listings, onMarkerClick, selectedListingId, classNa
function showPopup(map: mapboxgl.Map, marker: MapMarker) {
popupRef.current?.remove();
- const popup = new mapboxgl.Popup({ offset: 25, maxWidth: '260px', closeButton: true })
+ const popup = new mapboxgl.Popup({ offset: 25, maxWidth: 'min(260px, 85vw)', closeButton: true })
.setLngLat([marker.lng, marker.lat])
.setDOMContent(buildPopupContent(marker.listing))
.addTo(map);
@@ -209,7 +209,7 @@ export function ListingMap({ listings, onMarkerClick, selectedListingId, classNa
const hasToken = typeof process !== 'undefined' && process.env['NEXT_PUBLIC_MAPBOX_TOKEN'];
return (
-
+
{/* Fallback when no Mapbox token */}
diff --git a/apps/web/components/search/filter-bar.tsx b/apps/web/components/search/filter-bar.tsx
index da63610..ad70b65 100644
--- a/apps/web/components/search/filter-bar.tsx
+++ b/apps/web/components/search/filter-bar.tsx
@@ -83,7 +83,7 @@ export function FilterBar({ filters, onChange, onSearch, layout = 'horizontal' }