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
|
|
@ -26,7 +26,8 @@ import { useDecisions } from '@/hooks/useDecisions';
|
|||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { MobileBottomSheet } from './components/MobileBottomSheet';
|
||||
import { SwipeReviewMode } from './components/SwipeReviewMode';
|
||||
import { SavedView } from './components/SavedView';
|
||||
import { FavoritesView } from './components/FavoritesView';
|
||||
import { ListingDetailSheet } from './components/ListingDetailSheet';
|
||||
|
||||
function isTerminalStatus(status: string): boolean {
|
||||
return status === 'SUCCESS' || status === 'FAILURE' || status === 'REVOKED';
|
||||
|
|
@ -54,8 +55,9 @@ function App() {
|
|||
const [poiTravelFilters, setPoiTravelFilters] = useState<Record<number, POITravelFilter>>({});
|
||||
const [currentMetric, setCurrentMetric] = useState<Metric>(DEFAULT_FILTER_VALUES.metric);
|
||||
const isMobile = useIsMobile();
|
||||
const [activeCardFeature, setActiveCardFeature] = useState<PropertyFeature | null>(null);
|
||||
const [, setActiveCardFeature] = useState<PropertyFeature | null>(null);
|
||||
const [showReviewMode, setShowReviewMode] = useState(false);
|
||||
const [selectedListingId, setSelectedListingId] = useState<number | null>(null);
|
||||
|
||||
// Decision state (like/dislike)
|
||||
const { decide, clear, getDecision, likedCount, isLoaded: isDecisionsLoaded } = useDecisions(user);
|
||||
|
|
@ -364,9 +366,11 @@ function App() {
|
|||
|
||||
if (viewMode === 'saved') {
|
||||
return (
|
||||
<SavedView
|
||||
listingData={processedListingData}
|
||||
<FavoritesView
|
||||
listingData={listingData!}
|
||||
getDecision={getDecision}
|
||||
onSelectListing={(id) => setSelectedListingId(id)}
|
||||
onRemoveFavorite={(id, type) => clear(id, type)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -397,6 +401,10 @@ function App() {
|
|||
onPropertyClick={handlePropertyClick}
|
||||
highlightedPropertyUrl={highlightedProperty}
|
||||
poiMetricSelection={poiMetricSelection}
|
||||
onSelectListing={(id) => setSelectedListingId(id)}
|
||||
onSwipeRight={(id) => decide(id, 'liked', (queryParameters?.listing_type || 'RENT') as 'RENT' | 'BUY')}
|
||||
onSwipeLeft={(id) => decide(id, 'disliked', (queryParameters?.listing_type || 'RENT') as 'RENT' | 'BUY')}
|
||||
getDecision={(id) => getDecision(id, queryParameters?.listing_type || 'RENT')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -621,6 +629,17 @@ function App() {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Listing Detail Bottom Sheet */}
|
||||
<ListingDetailSheet
|
||||
user={user}
|
||||
listingId={selectedListingId}
|
||||
listingType={(queryParameters?.listing_type || 'RENT') as 'RENT' | 'BUY'}
|
||||
onClose={() => setSelectedListingId(null)}
|
||||
onDecide={(id, decision, type) => decide(id, decision, type)}
|
||||
onClearDecision={(id, type) => clear(id, type)}
|
||||
currentDecision={selectedListingId ? getDecision(selectedListingId, queryParameters?.listing_type || 'RENT') : undefined}
|
||||
/>
|
||||
|
||||
{/* Error Dialog */}
|
||||
<AlertError message={submitError} open={alertDialogIsOpen} setIsOpen={setAlertDialogIsOpen} />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue