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:
parent
9e1beb7495
commit
a2745c1478
14 changed files with 755 additions and 19 deletions
42
frontend/src/hooks/useListingDetail.ts
Normal file
42
frontend/src/hooks/useListingDetail.ts
Normal 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 };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue