Add photo carousel to swipe cards
SwipeCard now shows all available photos (up to 5) using embla-carousel instead of just the thumbnail. Includes prev/next arrow buttons and dot indicators. The photo area uses touch-action: pan-x so carousel swipes don't trigger card swipes.
This commit is contained in:
parent
a742f9bb65
commit
611449d328
1 changed files with 54 additions and 7 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import { useRef } from 'react';
|
||||
import { useRef, useState, useCallback, useEffect } from 'react';
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
import { useDrag } from '@use-gesture/react';
|
||||
import { Bed, Maximize2, ExternalLink } from 'lucide-react';
|
||||
import useEmblaCarousel from 'embla-carousel-react';
|
||||
import { Bed, Maximize2, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import type { PropertyFeature } from '@/types';
|
||||
|
||||
interface SwipeCardProps {
|
||||
|
|
@ -17,6 +18,21 @@ const SWIPE_THRESHOLD = 100;
|
|||
export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeCardProps) {
|
||||
const hasSwiped = useRef(false);
|
||||
const p = feature.properties;
|
||||
const photos = p.photos?.length ? p.photos : p.photo_thumbnail ? [p.photo_thumbnail] : [];
|
||||
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
|
||||
const [selectedPhoto, setSelectedPhoto] = useState(0);
|
||||
|
||||
const onPhotoSelect = useCallback(() => {
|
||||
if (!emblaApi) return;
|
||||
setSelectedPhoto(emblaApi.selectedScrollSnap());
|
||||
}, [emblaApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return;
|
||||
emblaApi.on('select', onPhotoSelect);
|
||||
return () => { emblaApi.off('select', onPhotoSelect); };
|
||||
}, [emblaApi, onPhotoSelect]);
|
||||
|
||||
const [style, api] = useSpring(() => ({
|
||||
x: 0,
|
||||
|
|
@ -96,11 +112,42 @@ export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeC
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Photo */}
|
||||
<div className="h-48 bg-muted relative">
|
||||
{p.photo_thumbnail && (
|
||||
<img src={p.photo_thumbnail} alt="Property" className="w-full h-full object-cover" />
|
||||
)}
|
||||
{/* Photo carousel */}
|
||||
<div className="h-48 bg-muted relative" style={{ touchAction: 'pan-x' }}>
|
||||
{photos.length > 1 ? (
|
||||
<>
|
||||
<div className="overflow-hidden h-full" ref={emblaRef}>
|
||||
<div className="flex h-full">
|
||||
{photos.map((url, i) => (
|
||||
<div key={i} className="flex-[0_0_100%] min-w-0 h-full">
|
||||
<img src={url} alt={`Photo ${i + 1}`} className="w-full h-full object-cover" loading="lazy" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="absolute left-1 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full p-1 z-20 transition-colors"
|
||||
onClick={(e) => { e.stopPropagation(); emblaApi?.scrollPrev(); }}
|
||||
aria-label="Previous photo"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
className="absolute right-10 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full p-1 z-20 transition-colors"
|
||||
onClick={(e) => { e.stopPropagation(); emblaApi?.scrollNext(); }}
|
||||
aria-label="Next photo"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="absolute bottom-1 left-0 right-0 flex justify-center gap-1 z-20">
|
||||
{photos.map((_, i) => (
|
||||
<div key={i} className={`w-1.5 h-1.5 rounded-full ${i === selectedPhoto ? 'bg-white' : 'bg-white/40'}`} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : photos.length === 1 ? (
|
||||
<img src={photos[0]} alt="Property" className="w-full h-full object-cover" />
|
||||
) : null}
|
||||
<button
|
||||
className="absolute top-3 right-3 bg-background/80 backdrop-blur-sm rounded-full p-2 z-20"
|
||||
onClick={(e) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue