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 { animated, useSpring } from '@react-spring/web';
|
||||||
import { useDrag } from '@use-gesture/react';
|
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';
|
import type { PropertyFeature } from '@/types';
|
||||||
|
|
||||||
interface SwipeCardProps {
|
interface SwipeCardProps {
|
||||||
|
|
@ -17,6 +18,21 @@ const SWIPE_THRESHOLD = 100;
|
||||||
export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeCardProps) {
|
export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeCardProps) {
|
||||||
const hasSwiped = useRef(false);
|
const hasSwiped = useRef(false);
|
||||||
const p = feature.properties;
|
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(() => ({
|
const [style, api] = useSpring(() => ({
|
||||||
x: 0,
|
x: 0,
|
||||||
|
|
@ -96,11 +112,42 @@ export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeC
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Photo */}
|
{/* Photo carousel */}
|
||||||
<div className="h-48 bg-muted relative">
|
<div className="h-48 bg-muted relative" style={{ touchAction: 'pan-x' }}>
|
||||||
{p.photo_thumbnail && (
|
{photos.length > 1 ? (
|
||||||
<img src={p.photo_thumbnail} alt="Property" className="w-full h-full object-cover" />
|
<>
|
||||||
)}
|
<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
|
<button
|
||||||
className="absolute top-3 right-3 bg-background/80 backdrop-blur-sm rounded-full p-2 z-20"
|
className="absolute top-3 right-3 bg-background/80 backdrop-blur-sm rounded-full p-2 z-20"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue