import { useRef, useState, useEffect, useCallback } from 'react' import { Plus } from 'lucide-react' import JourneyMap from './JourneyMap' import MobileEntryCard from './MobileEntryCard' import type { JourneyMapHandle } from './JourneyMap' import type { JourneyEntry } from '../../store/journeyStore' interface MapEntry { id: string lat: number lng: number title?: string | null mood?: string | null entry_date: string } interface Props { entries: JourneyEntry[] | any[] mapEntries: MapEntry[] trail?: { lat: number; lng: number }[] dark?: boolean readOnly?: boolean onEntryClick: (entry: any) => void onAddEntry?: () => void publicPhotoUrl?: (photoId: number) => string } export default function MobileMapTimeline({ entries, mapEntries, trail, dark, readOnly, onEntryClick, onAddEntry, publicPhotoUrl, }: Props) { const mapRef = useRef(null) const carouselRef = useRef(null) const [activeIndex, setActiveIndex] = useState(0) const cardRefs = useRef>(new Map()) const activeIndexRef = useRef(activeIndex) useEffect(() => { activeIndexRef.current = activeIndex }, [activeIndex]) // Sync map focus when carousel scrolls (with guard for uninitialized map) const syncMapToCarousel = useCallback((index: number) => { const entry = entries[index] if (!entry) return const mapEntry = mapEntries.find(m => String(m.id) === String(entry.id)) if (mapEntry) { try { mapRef.current?.focusMarker(String(mapEntry.id)) } catch {} } else { try { mapRef.current?.highlightMarker(null) } catch {} } }, [entries, mapEntries]) // Pick the card that's currently closest to the carousel horizontal center. // More stable than IntersectionObserver thresholds when the active card can // drift toward the viewport edge with proximity snapping. const pickNearestCard = useCallback(() => { const el = carouselRef.current if (!el) return const containerCenter = el.getBoundingClientRect().left + el.clientWidth / 2 let bestIdx = 0 let bestDist = Infinity cardRefs.current.forEach((node, idx) => { const r = node.getBoundingClientRect() const cardCenter = r.left + r.width / 2 const d = Math.abs(cardCenter - containerCenter) if (d < bestDist) { bestDist = d; bestIdx = idx } }) setActiveIndex(prev => { if (prev !== bestIdx) syncMapToCarousel(bestIdx) return bestIdx }) }, [syncMapToCarousel]) // Track scroll; debounce to re-center the active card when the user stops. useEffect(() => { const el = carouselRef.current if (!el || entries.length === 0) return let rafId: number | null = null let settleTimer: number | null = null const onScroll = () => { if (rafId != null) return rafId = requestAnimationFrame(() => { pickNearestCard() rafId = null }) if (settleTimer != null) window.clearTimeout(settleTimer) settleTimer = window.setTimeout(() => { // Ensure the active card sits at the center once the user settles. const card = cardRefs.current.get(activeIndexRef.current) card?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }) }, 180) } el.addEventListener('scroll', onScroll, { passive: true }) return () => { el.removeEventListener('scroll', onScroll) if (rafId != null) cancelAnimationFrame(rafId) if (settleTimer != null) window.clearTimeout(settleTimer) } }, [entries.length, pickNearestCard]) // Scroll a given card into the horizontal center of the carousel const scrollCardIntoCenter = useCallback((idx: number) => { const card = cardRefs.current.get(idx) card?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }) }, []) // Scroll carousel to entry when map marker is clicked const handleMarkerClick = useCallback((id: string) => { const idx = entries.findIndex((e: any) => String(e.id) === id) if (idx === -1) return setActiveIndex(idx) scrollCardIntoCenter(idx) }, [entries, scrollCardIntoCenter]) // Tap on a card: if it's already active, open the edit view; otherwise // activate + center it first (don't jump straight into the editor). const handleCardTap = useCallback((entry: any, idx: number) => { if (idx === activeIndex) { onEntryClick(entry) } else { setActiveIndex(idx) scrollCardIntoCenter(idx) } }, [activeIndex, onEntryClick, scrollCardIntoCenter]) // Initial map focus — delay to let Leaflet initialize and fitBounds useEffect(() => { if (entries.length > 0) { const timer = setTimeout(() => syncMapToCarousel(0), 500) return () => clearTimeout(timer) } }, [entries.length]) const activeEntryId = entries[activeIndex] ? String(entries[activeIndex].id) : null if (entries.length === 0) { return (
{!readOnly && onAddEntry && (
)}
) } return (
{/* Full-screen map */} {/* Bottom carousel */}
{entries.map((entry: any, i: number) => (
{ if (node) cardRefs.current.set(i, node); else cardRefs.current.delete(i); }} style={{ scrollSnapAlign: 'center' }} > handleCardTap(entry, i)} publicPhotoUrl={publicPhotoUrl} />
))}
{/* FAB: add entry — bottom right, above the timeline carousel */} {!readOnly && onAddEntry && (
)}
) }