feat: make frontend fully responsive with mobile-first layout
Add mobile-responsive design with full feature parity: - Bottom sheet (vaul) with 3 snap points for map+list coexistence - Swipeable property cards with horizontal scroll-snap - Hamburger menu with health, tasks, user info - Full-screen map with repositioned legend (top-left on mobile) - Filter FAB opening Sheet drawer - TaskProgressDrawer from bottom on mobile - All changes gated behind useIsMobile() hook (768px breakpoint) - Desktop layout completely untouched New components: MobileBottomSheet, SwipeableCardRow, PropertyCardCompact, MobileMenu Also fixes: idempotent longitude migration, React hooks order
This commit is contained in:
parent
8f068a581e
commit
a744b33578
14 changed files with 1768 additions and 152 deletions
87
frontend/src/components/SwipeableCardRow.tsx
Normal file
87
frontend/src/components/SwipeableCardRow.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { useRef, useEffect, useCallback } from 'react';
|
||||
import { PropertyCardCompact } from './PropertyCardCompact';
|
||||
import type { PropertyFeature } from '@/types';
|
||||
|
||||
interface SwipeableCardRowProps {
|
||||
features: PropertyFeature[];
|
||||
activeIndex: number;
|
||||
onActiveIndexChange: (index: number) => void;
|
||||
avgPricePerSqm: number;
|
||||
highlightedPropertyUrl?: string | null;
|
||||
onCardClick?: (feature: PropertyFeature) => void;
|
||||
}
|
||||
|
||||
export function SwipeableCardRow({
|
||||
features,
|
||||
activeIndex,
|
||||
onActiveIndexChange,
|
||||
avgPricePerSqm,
|
||||
highlightedPropertyUrl,
|
||||
onCardClick,
|
||||
}: SwipeableCardRowProps) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const isScrollingProgrammatically = useRef(false);
|
||||
|
||||
// Scroll to active index when it changes externally (e.g., from map marker tap)
|
||||
useEffect(() => {
|
||||
const container = scrollRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const cardWidth = 280 + 12; // card width + gap
|
||||
const targetScroll = activeIndex * cardWidth - (container.clientWidth - 280) / 2;
|
||||
|
||||
isScrollingProgrammatically.current = true;
|
||||
container.scrollTo({ left: targetScroll, behavior: 'smooth' });
|
||||
|
||||
// Reset flag after scroll completes
|
||||
const timer = setTimeout(() => {
|
||||
isScrollingProgrammatically.current = false;
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeIndex]);
|
||||
|
||||
// Detect which card is centered after user scrolls
|
||||
const handleScroll = useCallback(() => {
|
||||
if (isScrollingProgrammatically.current) return;
|
||||
const container = scrollRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const cardWidth = 280 + 12;
|
||||
const centerX = container.scrollLeft + container.clientWidth / 2;
|
||||
const newIndex = Math.round((centerX - 280 / 2) / cardWidth);
|
||||
const clampedIndex = Math.max(0, Math.min(newIndex, features.length - 1));
|
||||
|
||||
if (clampedIndex !== activeIndex) {
|
||||
onActiveIndexChange(clampedIndex);
|
||||
}
|
||||
}, [activeIndex, features.length, onActiveIndexChange]);
|
||||
|
||||
// Debounced scroll handler
|
||||
const scrollTimerRef = useRef<number | null>(null);
|
||||
const debouncedScroll = useCallback(() => {
|
||||
if (scrollTimerRef.current) clearTimeout(scrollTimerRef.current);
|
||||
scrollTimerRef.current = window.setTimeout(handleScroll, 100);
|
||||
}, [handleScroll]);
|
||||
|
||||
if (features.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={debouncedScroll}
|
||||
className="flex gap-3 overflow-x-auto snap-x snap-mandatory px-4 py-2 scrollbar-none"
|
||||
style={{ WebkitOverflowScrolling: 'touch' }}
|
||||
>
|
||||
{features.map((feature, index) => (
|
||||
<PropertyCardCompact
|
||||
key={feature.properties.url}
|
||||
property={feature.properties}
|
||||
isActive={index === activeIndex}
|
||||
isHighlighted={feature.properties.url === highlightedPropertyUrl}
|
||||
avgPricePerSqm={avgPricePerSqm}
|
||||
onClick={() => onCardClick?.(feature)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue