Add tappable cards, detail bottom sheet, swipe gestures, and favorites view

- Decision types, services (decisionService, listingDetailService), and index exports
- useDecisions hook with optimistic updates and Map-based state
- useListingDetail hook with session-level caching
- PhotoCarousel component using embla-carousel-react
- ListingDetail component with full property info, like/dislike buttons
- ListingDetailSheet using vaul Drawer (slide-up bottom sheet)
- SwipeablePropertyCard with @use-gesture/react and @react-spring/web
- SwipeReviewMode for mobile full-screen swipe review
- FavoritesView with virtualized liked listings and remove button
- App.tsx integration: decision state, client-side disliked filtering, detail sheet, swipe handlers
- ListView conditionally renders SwipeablePropertyCard when handlers provided
- StatsBar adds 'saved' view mode with heart icon
- Header adds liked count indicator
- New deps: vaul, embla-carousel-react, @use-gesture/react, @react-spring/web
This commit is contained in:
Viktor Barzin 2026-02-21 15:48:17 +00:00
parent 9e1beb7495
commit a2745c1478
No known key found for this signature in database
GPG key ID: 0EB088298288D958
14 changed files with 755 additions and 19 deletions

View file

@ -0,0 +1,42 @@
import { useState, useRef, useCallback } from 'react';
import type { AuthUser } from '@/auth/types';
import type { ListingDetailData } from '@/types';
import { fetchListingDetail } from '@/services';
export function useListingDetail(user: AuthUser | null) {
const [detail, setDetail] = useState<ListingDetailData | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const cache = useRef<Map<string, ListingDetailData>>(new Map());
const loadDetail = useCallback(
async (listingId: number, listingType: 'RENT' | 'BUY' = 'RENT') => {
if (!user) return;
const cacheKey = `${listingId}-${listingType}`;
const cached = cache.current.get(cacheKey);
if (cached) {
setDetail(cached);
return;
}
setIsLoading(true);
setError(null);
try {
const data = await fetchListingDetail(user, listingId, listingType);
cache.current.set(cacheKey, data);
setDetail(data);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setIsLoading(false);
}
},
[user],
);
const clearDetail = useCallback(() => {
setDetail(null);
setError(null);
}, []);
return { detail, isLoading, error, loadDetail, clearDetail };
}