From cf2bed8ab1cd33bf7e27cb04f4b2d5ef0ffc020a Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 6 May 2026 12:24:13 -0700 Subject: [PATCH 1/2] RR-T45 POI fixes --- jest-setup.ts | 26 ++ package.json | 4 +- src/app/(app)/__tests__/map.test.tsx | 19 +- src/app/(app)/map.tsx | 73 +---- src/app/(app)/messages.tsx | 26 +- src/app/_layout.tsx | 22 +- src/app/call/[id]/index.tsx | 2 +- src/components/maps/map-panel.tsx | 7 +- src/components/maps/map-pins.tsx | 54 +++- src/components/maps/pin-marker.tsx | 5 +- .../maps/poi-filter-bottom-sheet.tsx | 149 ++++++++++ src/components/maps/poi-icon.tsx | 131 +++++++++ src/components/maps/poi-list-panel.tsx | 118 ++++---- src/components/maps/poi-pin-marker.tsx | 88 ++++++ src/components/ui/shared-tabs.tsx | 31 +- src/constants/map-icons.ts | 17 +- src/constants/poi-icon-mapping.ts | 200 +++++++++++++ src/constants/poi-marker-shapes.ts | 91 ++++++ src/lib/logging/index.tsx | 17 ++ src/lib/poi.ts | 48 ++- src/lib/utils.ts | 4 +- .../v4/mapping/getMapDataAndMarkersData.ts | 1 + src/models/v4/mapping/poiLayerData.ts | 1 + src/models/v4/mapping/poiResultData.ts | 1 + src/models/v4/mapping/poiTypeResultData.ts | 1 + src/translations/ar.json | 3 + src/translations/de.json | 3 + src/translations/en.json | 3 + src/translations/es.json | 3 + src/translations/fr.json | 3 + src/translations/it.json | 3 + src/translations/pl.json | 3 + src/translations/sv.json | 3 + src/translations/uk.json | 3 + yarn.lock | 275 +++++++++--------- 35 files changed, 1117 insertions(+), 321 deletions(-) create mode 100644 src/components/maps/poi-filter-bottom-sheet.tsx create mode 100644 src/components/maps/poi-icon.tsx create mode 100644 src/components/maps/poi-pin-marker.tsx create mode 100644 src/constants/poi-icon-mapping.ts create mode 100644 src/constants/poi-marker-shapes.ts diff --git a/jest-setup.ts b/jest-setup.ts index 419cfea..b6a8e2a 100644 --- a/jest-setup.ts +++ b/jest-setup.ts @@ -788,6 +788,32 @@ jest.mock('expo-asset', () => ({ }, })); +// Mock @sentry/react-native to prevent native module errors in tests +jest.mock('@sentry/react-native', () => { + const breadcrumbs: any[] = []; + + return { + __esModule: true, + init: jest.fn().mockResolvedValue(undefined), + wrap: jest.fn((component: any) => component), + captureException: jest.fn(), + captureMessage: jest.fn(), + addBreadcrumb: jest.fn().mockImplementation((breadcrumb: any) => { + breadcrumbs.push(breadcrumb); + }), + reactNavigationIntegration: jest.fn(() => ({ + registerNavigationContainer: jest.fn(), + })), + ErrorBoundary: ({ children }: any) => children, + setUser: jest.fn(), + setTag: jest.fn(), + setExtra: jest.fn(), + isInitialized: jest.fn().mockReturnValue(true), + // Re-export breadcrumbs for test assertions + _breadcrumbs: breadcrumbs, + }; +}); + // Mock expo-av to avoid import issues jest.mock('expo-av', () => ({ Audio: { diff --git a/package.json b/package.json index ceecab3..d17815e 100644 --- a/package.json +++ b/package.json @@ -88,14 +88,14 @@ "@react-native-community/netinfo": "11.4.1", "@rnmapbox/maps": "10.2.10", "@semantic-release/git": "^10.0.1", - "@sentry/react-native": "~7.2.0", + "@sentry/react-native": "~8.10.0", "@shopify/flash-list": "2.0.2", "@tanstack/react-query": "~5.52.1", "app-icon-badge": "^0.1.2", "axios": "^1.12.0", "babel-plugin-module-resolver": "^5.0.2", "buffer": "^6.0.3", - "countly-sdk-react-native-bridge": "^25.4.0", + "countly-sdk-react-native-bridge": "^25.4.1", "crypto-js": "^4.2.0", "date-fns": "^4.1.0", "expo": "^54.0.33", diff --git a/src/app/(app)/__tests__/map.test.tsx b/src/app/(app)/__tests__/map.test.tsx index 108c4c3..a0b1265 100644 --- a/src/app/(app)/__tests__/map.test.tsx +++ b/src/app/(app)/__tests__/map.test.tsx @@ -353,7 +353,7 @@ describe('HomeMap', () => { expect(screen.getByTestId('map-camera')).toBeTruthy(); }); - it('shows side menu in landscape mode', async () => { + it('renders in landscape mode', async () => { // Mock landscape dimensions const mockUseWindowDimensions = (jest.requireMock('react-native') as any).useWindowDimensions; mockUseWindowDimensions.mockReturnValue({ @@ -368,11 +368,12 @@ describe('HomeMap', () => { expect(screen.getByTestId('map-pins')).toBeTruthy(); }); - // In landscape mode, side menu should be permanently visible - expect(screen.getByTestId('side-menu')).toBeTruthy(); + // In landscape mode, the map container should still render correctly + // (the side menu is now handled by the parent _layout.tsx, not the map page itself) + expect(screen.getByTestId('home-map-container')).toBeTruthy(); }); - it('shows drawer in portrait mode when opened', async () => { + it('does not render its own drawer in portrait mode', async () => { render(); // Wait for async map data to load @@ -380,11 +381,8 @@ describe('HomeMap', () => { expect(screen.getByTestId('map-pins')).toBeTruthy(); }); - // Initially drawer should not be visible + // The drawer is now handled by the parent _layout.tsx, not the map page itself expect(screen.queryByTestId('drawer')).toBeNull(); - - // Since there's no header menu button, we can't test opening the drawer - // This test would need to be modified based on how the drawer is actually opened }); it('shows recenter button when user has moved map and location is available', async () => { @@ -548,8 +546,9 @@ describe('HomeMap', () => { expect(screen.getByTestId('map-pins')).toBeTruthy(); }); - // In landscape mode, side menu should be permanently visible - expect(screen.getByTestId('side-menu')).toBeTruthy(); + // In landscape mode, the map container should still render correctly + // (side menu is handled by the parent _layout.tsx) + expect(screen.getByTestId('home-map-container')).toBeTruthy(); }); describe('Analytics Tracking', () => { diff --git a/src/app/(app)/map.tsx b/src/app/(app)/map.tsx index c4b64e7..bb9fa47 100644 --- a/src/app/(app)/map.tsx +++ b/src/app/(app)/map.tsx @@ -1,14 +1,10 @@ import { useLocalSearchParams, useRouter } from 'expo-router'; -import { Menu } from 'lucide-react-native'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { TouchableOpacity, useWindowDimensions, View } from 'react-native'; +import { useWindowDimensions, View } from 'react-native'; import MapPanel from '@/components/maps/map-panel'; import PoiListPanel from '@/components/maps/poi-list-panel'; -import { SideMenu } from '@/components/sidebar/side-menu'; -import { Button, ButtonText } from '@/components/ui/button'; -import { Drawer, DrawerBackdrop, DrawerBody, DrawerContent, DrawerFooter } from '@/components/ui/drawer/index'; import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar'; import { SharedTabs, type TabItem } from '@/components/ui/shared-tabs'; import { type PoiResultData } from '@/models/v4/mapping/poiResultData'; @@ -20,7 +16,6 @@ export default function HomeMap() { const { tab, poiId } = useLocalSearchParams<{ tab?: string | string[]; poiId?: string | string[] }>(); const { width, height } = useWindowDimensions(); const isLandscape = width > height; - const [isSideMenuOpen, setIsSideMenuOpen] = useState(false); const fetchPoisData = usePoiStore((state) => state.fetchPoisData); useEffect(() => { @@ -80,59 +75,21 @@ export default function HomeMap() { - {/* Content Area with Side Menu */} - - {/* Permanent Side Menu in Landscape */} - {isLandscape && ( - - - - )} - - {/* Map Content */} - - {/* Portrait menu button */} - {!isLandscape && ( - setIsSideMenuOpen(true)} - > - - - )} - - + {/* Map Content */} + + - - {/* Drawer for Portrait Mode */} - {!isLandscape && ( - setIsSideMenuOpen(false)} size="lg"> - setIsSideMenuOpen(false)} /> - - - setIsSideMenuOpen(false)} /> - - - - - - - )} ); } diff --git a/src/app/(app)/messages.tsx b/src/app/(app)/messages.tsx index 25ef31a..b4dc09e 100644 --- a/src/app/(app)/messages.tsx +++ b/src/app/(app)/messages.tsx @@ -1,22 +1,20 @@ import { useFocusEffect } from '@react-navigation/native'; import { router, Stack } from 'expo-router'; -import { ChevronDown, Mail, MailOpen, Menu, MessageSquarePlus, MoreVertical, Search, Trash2, X } from 'lucide-react-native'; +import { ChevronDown, Mail, MailOpen, MessageSquarePlus, MoreVertical, Search, Trash2, X } from 'lucide-react-native'; import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Alert, useWindowDimensions } from 'react-native'; +import { Alert } from 'react-native'; import { Loading } from '@/components/common/loading'; import ZeroState from '@/components/common/zero-state'; import { ComposeMessageSheet } from '@/components/messages/compose-message-sheet'; import { MessageCard } from '@/components/messages/message-card'; import { MessageDetailsSheet } from '@/components/messages/message-details-sheet'; -import { SideMenu } from '@/components/sidebar/side-menu'; import { FocusAwareStatusBar, View } from '@/components/ui'; import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetItem, ActionsheetItemText } from '@/components/ui/actionsheet'; import { Badge } from '@/components/ui/badge'; import { Button, ButtonText } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; -import { Drawer, DrawerBackdrop, DrawerBody, DrawerContent, DrawerFooter } from '@/components/ui/drawer/index'; import { Fab, FabIcon } from '@/components/ui/fab'; import { FlatList } from '@/components/ui/flat-list'; import { HStack } from '@/components/ui/hstack'; @@ -32,11 +30,8 @@ import { useSecurityStore } from '@/stores/security/store'; export default function MessagesScreen() { const { t } = useTranslation(); - const { width, height } = useWindowDimensions(); - const isLandscape = width > height; const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false); const [isSelectionMode, setIsSelectionMode] = useState(false); - const [isSideMenuOpen, setIsSideMenuOpen] = useState(false); const { canUserCreateMessages } = useSecurityStore(); const { trackEvent } = useAnalytics(); @@ -211,13 +206,6 @@ export default function MessagesScreen() { - !isLandscape ? ( - setIsSideMenuOpen(true)} testID="messages-menu-button"> - - - ) : null, - headerRight: () => null, }} /> @@ -331,16 +319,6 @@ export default function MessagesScreen() { {isLoading && filteredMessages.length === 0 && } - {/* Side Menu Drawer */} - setIsSideMenuOpen(false)} size={isLandscape ? 'md' : 'lg'}> - setIsSideMenuOpen(false)} /> - - - setIsSideMenuOpen(false)} /> - - - - {/* Filter Action Sheet */} setIsFilterMenuOpen(false)}> diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index 5c8c091..44c7d2a 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -55,21 +55,33 @@ const navigationIntegration = Sentry.reactNavigationIntegration({ enableTimeToInitialDisplay: false, }); +// Validate DSN is configured — warn early if missing +if (!Env.SENTRY_DSN) { + console.warn('[Sentry] DSN is empty — errors and profiles will NOT be sent. Set RESPOND_SENTRY_DSN in your environment.'); +} + Sentry.init({ dsn: Env.SENTRY_DSN, debug: __DEV__, // Only debug in development, not production - tracesSampleRate: __DEV__ ? 1.0 : 0.2, // 100% in dev, 20% in production to reduce performance impact - profilesSampleRate: __DEV__ ? 1.0 : 0.2, // 100% in dev, 20% in production to reduce performance impact + // tracesSampleRate: percentage of transactions sent (1.0 = 100%, 0.2 = 20%) + tracesSampleRate: __DEV__ ? 1.0 : 0.2, + // profilesSampleRate: relative to tracesSampleRate! + // Effective profile rate = tracesSampleRate * profilesSampleRate. + // In production: 0.2 * 1.0 = 20% of all transactions get profiles. + // Set to 1.0 to profile ALL sampled transactions, or lower to further sub-sample. + profilesSampleRate: __DEV__ ? 1.0 : 1.0, sendDefaultPii: false, + enableAppHangTracking: true, // v8: Tracks app freeze/hang events + enableWatchdogTerminationTracking: true, // v8: Tracks iOS watchdog terminations integrations: [ // Pass integration navigationIntegration, ], - enableNativeFramesTracking: true, //!isRunningInExpoGo(), // Tracks slow and frozen frames in the application + enableNativeFramesTracking: true, // Tracks slow and frozen frames // Add additional options to prevent timing issues - beforeSendTransaction(event: any) { + beforeSend(event) { // Filter out problematic navigation transactions that might cause timestamp errors - if (event.contexts?.trace?.op === 'navigation' && !event.contexts?.trace?.data?.route) { + if (event.type === 'transaction' && event.contexts?.trace?.op === 'navigation' && !event.contexts?.trace?.data?.route) { return null; } return event; diff --git a/src/app/call/[id]/index.tsx b/src/app/call/[id]/index.tsx index 178bf9b..0b88a47 100644 --- a/src/app/call/[id]/index.tsx +++ b/src/app/call/[id]/index.tsx @@ -774,7 +774,7 @@ export default function CallDetail() { {/* Tabs */} - + setIsNotesModalOpen(false)} callId={callId || ''} /> diff --git a/src/components/maps/map-panel.tsx b/src/components/maps/map-panel.tsx index 8602e60..dd60bfd 100644 --- a/src/components/maps/map-panel.tsx +++ b/src/components/maps/map-panel.tsx @@ -10,6 +10,7 @@ import { Loading } from '@/components/common/loading'; import { useAnalytics } from '@/hooks/use-analytics'; import { useMapSignalRUpdates } from '@/hooks/use-map-signalr-updates'; import { logger } from '@/lib/logging'; +import { isPoiMarker } from '@/lib/poi'; import { onSortOptions } from '@/lib/utils'; import { type MapMakerInfoData } from '@/models/v4/mapping/getMapDataAndMarkersData'; import { type PoiResultData } from '@/models/v4/mapping/poiResultData'; @@ -20,8 +21,6 @@ import { useToastStore } from '@/stores/toast/store'; import MapPins from './map-pins'; import PinDetailModal from './pin-detail-modal'; -const POI_MARKER_TYPE = 4; - interface MapPanelProps { focusedPoi: PoiResultData | null; } @@ -266,9 +265,7 @@ export const MapPanel: React.FC = ({ focusedPoi }) => { pinType: pin.Type, }); - const isPoiPin = pin.Type === POI_MARKER_TYPE || pin.PoiTypeId != null; - - if (isPoiPin) { + if (isPoiMarker(pin)) { router.push(`/poi/${pin.Id}`); return; } diff --git a/src/components/maps/map-pins.tsx b/src/components/maps/map-pins.tsx index a8a2787..cf31410 100644 --- a/src/components/maps/map-pins.tsx +++ b/src/components/maps/map-pins.tsx @@ -1,16 +1,32 @@ import Mapbox from '@rnmapbox/maps'; +import { useColorScheme } from 'nativewind'; import React from 'react'; +import { StyleSheet, Text } from 'react-native'; +import { POI_MARKER_HEIGHT, POI_MARKER_WIDTH } from '@/constants/poi-marker-shapes'; +import { isPoiMarker } from '@/lib/poi'; import { type MapMakerInfoData } from '@/models/v4/mapping/getMapDataAndMarkersData'; import { useSecurityStore } from '@/stores/security/store'; import PinMarker from './pin-marker'; +import PoiPinMarker from './poi-pin-marker'; interface MapPinsProps { pins: MapMakerInfoData[]; onPinPress?: (pin: MapMakerInfoData) => void; } +const PoiTitle: React.FC<{ title: string }> = ({ title }) => { + const { colorScheme } = useColorScheme(); + const isDark = colorScheme === 'dark'; + + return ( + + {title} + + ); +}; + const MapPins: React.FC = ({ pins, onPinPress }) => { const { canUserViewPII } = useSecurityStore(); @@ -29,13 +45,41 @@ const MapPins: React.FC = ({ pins, onPinPress }) => { return ( <> - {filteredPins.map((pin) => ( - - onPinPress?.(pin)} /> - - ))} + {filteredPins.map((pin) => { + if (isPoiMarker(pin)) { + return ( + + {/* Shape + icon: 36x48 exact dimensions, anchor at bottom-center tip */} + + onPinPress?.(pin)} /> + + {/* Title label: separate MarkerView so it does NOT affect shape anchor measurement */} + {pin.Title ? ( + + + + ) : null} + + ); + } + + return ( + + onPinPress?.(pin)} /> + + ); + })} ); }; +const styles = StyleSheet.create({ + poiTitle: { + fontSize: 10, + fontWeight: '600', + textAlign: 'center', + maxWidth: POI_MARKER_WIDTH + 20, + }, +}); + export default MapPins; diff --git a/src/components/maps/pin-marker.tsx b/src/components/maps/pin-marker.tsx index 1a528cd..b8a45f5 100644 --- a/src/components/maps/pin-marker.tsx +++ b/src/components/maps/pin-marker.tsx @@ -7,6 +7,7 @@ import { MAP_ICONS, type MapIconKey, resolveMapIconKey } from '@/constants/map-i interface PinMarkerProps { imagePath?: string | null; + poiImage?: string | null; marker?: string | null; title: string; size?: number; @@ -15,10 +16,10 @@ interface PinMarkerProps { fallbackIconKey?: MapIconKey; } -const PinMarker: React.FC = ({ imagePath, marker, title, size = 32, onPress, fallbackIconKey = 'call' }) => { +const PinMarker: React.FC = ({ imagePath, poiImage, marker, title, size = 32, onPress, fallbackIconKey = 'call' }) => { const { colorScheme } = useColorScheme(); - const icon = MAP_ICONS[resolveMapIconKey({ imagePath, marker, fallback: fallbackIconKey })]; + const icon = MAP_ICONS[resolveMapIconKey({ poiImage, imagePath, marker, fallback: fallbackIconKey })]; return ( diff --git a/src/components/maps/poi-filter-bottom-sheet.tsx b/src/components/maps/poi-filter-bottom-sheet.tsx new file mode 100644 index 0000000..ce69559 --- /dev/null +++ b/src/components/maps/poi-filter-bottom-sheet.tsx @@ -0,0 +1,149 @@ +import { ChevronDownIcon, FilterIcon, RotateCcwIcon, XIcon } from 'lucide-react-native'; +import { useColorScheme } from 'nativewind'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { type PoiTypeResultData } from '@/models/v4/mapping/poiTypeResultData'; + +import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetDragIndicator, ActionsheetDragIndicatorWrapper } from '../ui/actionsheet'; +import { Badge } from '../ui/badge'; +import { Box } from '../ui/box'; +import { Button, ButtonText } from '../ui/button'; +import { Heading } from '../ui/heading'; +import { HStack } from '../ui/hstack'; +import { Select, SelectBackdrop, SelectContent, SelectDragIndicator, SelectDragIndicatorWrapper, SelectIcon, SelectInput, SelectItem, SelectPortal, SelectTrigger } from '../ui/select'; +import { Text } from '../ui/text'; +import { VStack } from '../ui/vstack'; + +type PoiSortOption = 'name' | 'type'; +const ALL_POI_TYPES_VALUE = 'all'; + +interface PoiFilterBottomSheetProps { + isOpen: boolean; + onClose: () => void; + selectedPoiType: string; + onPoiTypeChange: (value: string) => void; + sortBy: PoiSortOption; + onSortByChange: (value: PoiSortOption) => void; + poiTypes: PoiTypeResultData[]; +} + +export const PoiFilterBottomSheet: React.FC = ({ isOpen, onClose, selectedPoiType, onPoiTypeChange, sortBy, onSortByChange, poiTypes }) => { + const { t } = useTranslation(); + const { colorScheme } = useColorScheme(); + + const hasActiveFilters = selectedPoiType !== ALL_POI_TYPES_VALUE || sortBy !== 'name'; + + const handleReset = useCallback(() => { + onPoiTypeChange(ALL_POI_TYPES_VALUE); + onSortByChange('name'); + }, [onPoiTypeChange, onSortByChange]); + + const handleDone = useCallback(() => { + onClose(); + }, [onClose]); + + const handlePoiTypeChange = useCallback( + (value: string) => { + onPoiTypeChange(value); + }, + [onPoiTypeChange] + ); + + const handleSortChange = useCallback( + (value: string) => { + onSortByChange(value as PoiSortOption); + }, + [onSortByChange] + ); + + const selectedPoiTypeLabel = selectedPoiType === ALL_POI_TYPES_VALUE ? t('poi.filter_all_types') : poiTypes.find((pt) => pt.PoiTypeId.toString() === selectedPoiType)?.Name || t('poi.filter_all_types'); + + const selectedSortLabel = sortBy === 'name' ? t('poi.sort_name') : t('poi.sort_type'); + + return ( + + + + + + + + + + + + + {t('poi.filter_sort_title', 'Filter & Sort')} + + {hasActiveFilters ? ( + + ! + + ) : null} + + + + + + + {t('poi.filter_label')} + + + + + {t('poi.sort_label')} + + + + + + + + + + + + ); +}; + +export default PoiFilterBottomSheet; diff --git a/src/components/maps/poi-icon.tsx b/src/components/maps/poi-icon.tsx new file mode 100644 index 0000000..1961753 --- /dev/null +++ b/src/components/maps/poi-icon.tsx @@ -0,0 +1,131 @@ +import type { LucideIcon } from 'lucide-react-native'; +import * as LucideIcons from 'lucide-react-native'; +import React from 'react'; + +/** + * Maps a kebab-case lucide icon name string to the corresponding + * lucide-react-native icon component. + * + * This allows dynamic icon selection based on POI type data without + * importing every possible icon statically at the call site. + */ +const LUCIDE_ICON_MAP: Record = { + // Map marker icons + 'map-pin': LucideIcons.MapPin, + 'shield-alert': LucideIcons.ShieldAlert, + shield: LucideIcons.Shield, + 'shield-check': LucideIcons.ShieldCheck, + flame: LucideIcons.Flame, + route: LucideIcons.Route, + square: LucideIcons.Square, + flag: LucideIcons.Flag, + info: LucideIcons.Info, + landmark: LucideIcons.Landmark, + + // Transportation + car: LucideIcons.Car, + bike: LucideIcons.Bike, + bus: LucideIcons.Bus, + truck: LucideIcons.Truck, + ship: LucideIcons.Ship, + plane: LucideIcons.Plane, + 'train-front': LucideIcons.TrainFront, + + // Buildings & Places + building: LucideIcons.Building, + 'building-2': LucideIcons.Building2, + home: LucideIcons.Home, + hotel: LucideIcons.Hotel, + store: LucideIcons.Store, + warehouse: LucideIcons.Warehouse, + school: LucideIcons.School, + hospital: LucideIcons.Hospital, + church: LucideIcons.Church, + tent: LucideIcons.Tent, + 'door-open': LucideIcons.DoorOpen, + + // Nature & Outdoor + 'tree-pine': LucideIcons.TreePine, + mountain: LucideIcons.Mountain, + waves: LucideIcons.Waves, + droplets: LucideIcons.Droplets, + wind: LucideIcons.Wind, + fish: LucideIcons.Fish, + 'paw-print': LucideIcons.PawPrint, + flower: LucideIcons.Flower, + + // Food & Drink + utensils: LucideIcons.Utensils, + coffee: LucideIcons.Coffee, + beer: LucideIcons.Beer, + croissant: LucideIcons.Croissant, + 'shopping-basket': LucideIcons.ShoppingBasket, + 'shopping-cart': LucideIcons.ShoppingCart, + + // Commerce & Services + wrench: LucideIcons.Wrench, + hammer: LucideIcons.Hammer, + scissors: LucideIcons.Scissors, + key: LucideIcons.Key, + gem: LucideIcons.Gem, + scale: LucideIcons.Scale, + mail: LucideIcons.Mail, + phone: LucideIcons.Phone, + globe: LucideIcons.Globe, + monitor: LucideIcons.Monitor, + shirt: LucideIcons.Shirt, + armchair: LucideIcons.Armchair, + footprints: LucideIcons.Footprints, + paintbrush: LucideIcons.Paintbrush, + antenna: LucideIcons.Antenna, + + // Education + 'graduation-cap': LucideIcons.GraduationCap, + baby: LucideIcons.Baby, + library: LucideIcons.Library, + + // Recreation + 'ferris-wheel': LucideIcons.FerrisWheel, + palette: LucideIcons.Palette, + dices: LucideIcons.Dices, + clapperboard: LucideIcons.Clapperboard, + dumbbell: LucideIcons.Dumbbell, + music: LucideIcons.Music, + circle: LucideIcons.Circle, + 'circle-parking': LucideIcons.CircleParking, + 'pill-bottle': LucideIcons.PillBottle, + + // Utility + zap: LucideIcons.Zap, + bell: LucideIcons.Bell, + cross: LucideIcons.Cross, + stethoscope: LucideIcons.Stethoscope, + gavel: LucideIcons.Gavel, + fuel: LucideIcons.Fuel, + 'arrow-down': LucideIcons.ArrowDown, +}; + +interface PoiIconProps { + /** Kebab-case lucide icon name (e.g. "hospital", "map-pin") */ + name: string; + /** Icon size in pixels */ + size?: number; + /** Icon color */ + color?: string; +} + +const DEFAULT_ICON_SIZE = 14; +const DEFAULT_ICON_COLOR = '#ffffff'; + +/** + * Renders a lucide-react-native icon by its kebab-case name string. + * + * Falls back to the MapPin icon if the requested icon name is not found. + */ +export const PoiIcon: React.FC = ({ name, size = DEFAULT_ICON_SIZE, color = DEFAULT_ICON_COLOR }) => { + const IconComponent = LUCIDE_ICON_MAP[name] || LUCIDE_ICON_MAP['map-pin']; + + return ; +}; + +export default PoiIcon; diff --git a/src/components/maps/poi-list-panel.tsx b/src/components/maps/poi-list-panel.tsx index d6c9cd1..2de5c67 100644 --- a/src/components/maps/poi-list-panel.tsx +++ b/src/components/maps/poi-list-panel.tsx @@ -1,16 +1,16 @@ import { FlashList, type ListRenderItem } from '@shopify/flash-list'; -import { ChevronDownIcon, MapIcon } from 'lucide-react-native'; +import { FilterIcon, MapIcon, SearchIcon, XIcon } from 'lucide-react-native'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Loading } from '@/components/common/loading'; import ZeroState from '@/components/common/zero-state'; +import { PoiFilterBottomSheet } from '@/components/maps/poi-filter-bottom-sheet'; import { Badge } from '@/components/ui/badge'; import { Box } from '@/components/ui/box'; import { Button, ButtonText } from '@/components/ui/button'; import { HStack } from '@/components/ui/hstack'; -import { Pressable } from '@/components/ui/pressable'; -import { Select, SelectBackdrop, SelectContent, SelectDragIndicator, SelectDragIndicatorWrapper, SelectIcon, SelectInput, SelectItem, SelectPortal, SelectTrigger } from '@/components/ui/select'; +import { Input, InputField, InputIcon, InputSlot } from '@/components/ui/input'; import { Text } from '@/components/ui/text'; import { VStack } from '@/components/ui/vstack'; import { getPoiDisplayName } from '@/lib/poi'; @@ -88,9 +88,23 @@ export const PoiListPanel: React.FC = ({ onPoiPress, onViewOn })); const [selectedPoiType, setSelectedPoiType] = useState(ALL_POI_TYPES_VALUE); const [sortBy, setSortBy] = useState('name'); + const [searchQuery, setSearchQuery] = useState(''); + const [isFilterSheetOpen, setIsFilterSheetOpen] = useState(false); + + const openFilterSheet = useCallback(() => setIsFilterSheetOpen(true), []); + const closeFilterSheet = useCallback(() => setIsFilterSheetOpen(false), []); + + const hasActiveFilters = selectedPoiType !== ALL_POI_TYPES_VALUE || sortBy !== 'name'; const filteredPois = useMemo(() => { - const nextPois = selectedPoiType === ALL_POI_TYPES_VALUE ? [...pois] : pois.filter((poi) => poi.PoiTypeId.toString() === selectedPoiType); + let nextPois = selectedPoiType === ALL_POI_TYPES_VALUE ? [...pois] : pois.filter((poi) => poi.PoiTypeId.toString() === selectedPoiType); + + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + nextPois = nextPois.filter( + (poi) => poi.Name?.toLowerCase().includes(query) || poi.Address?.toLowerCase().includes(query) || poi.Note?.toLowerCase().includes(query) || poi.PoiTypeName?.toLowerCase().includes(query) + ); + } nextPois.sort((left, right) => { if (sortBy === 'type') { @@ -102,19 +116,7 @@ export const PoiListPanel: React.FC = ({ onPoiPress, onViewOn }); return nextPois; - }, [pois, selectedPoiType, sortBy]); - - const selectedPoiTypeLabel = useMemo(() => { - if (selectedPoiType === ALL_POI_TYPES_VALUE) { - return t('poi.filter_all_types'); - } - - return poiTypes.find((poiType) => poiType.PoiTypeId.toString() === selectedPoiType)?.Name || t('poi.filter_all_types'); - }, [poiTypes, selectedPoiType, t]); - - const selectedSortLabel = useMemo(() => { - return sortBy === 'name' ? t('poi.sort_name') : t('poi.sort_type'); - }, [sortBy, t]); + }, [pois, selectedPoiType, sortBy, searchQuery]); const renderPoiItem = useCallback>(({ item }) => , [onPoiPress, onViewOnMap]); @@ -130,61 +132,39 @@ export const PoiListPanel: React.FC = ({ onPoiPress, onViewOn return ( - - - {t('poi.list_description')} - - - - {t('poi.filter_label')} - - - - - {t('poi.sort_label')} - - - - - + {t('poi.list_description')} + + + + + + + + {searchQuery ? ( + setSearchQuery('')}> + + + ) : null} + + + {t('poi.results_count', { count: filteredPois.length })} - + - {t('poi.view_on_map_hint')} + + {t('poi.view_on_map_hint')} + @@ -193,6 +173,8 @@ export const PoiListPanel: React.FC = ({ onPoiPress, onViewOn ) : ( )} + + ); }; diff --git a/src/components/maps/poi-pin-marker.tsx b/src/components/maps/poi-pin-marker.tsx new file mode 100644 index 0000000..b563e91 --- /dev/null +++ b/src/components/maps/poi-pin-marker.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import Svg, { Path } from 'react-native-svg'; + +import { resolvePoiIcon } from '@/constants/poi-icon-mapping'; +import { getPoiMarkerShapePath, POI_MARKER_HEIGHT, POI_MARKER_VIEWBOX, POI_MARKER_WIDTH } from '@/constants/poi-marker-shapes'; + +import { PoiIcon } from './poi-icon'; + +interface PoiPinMarkerProps { + /** Hex color for the SVG shape fill (e.g. "#2563eb") */ + color?: string | null; + /** The `PoiImage` field from the API (e.g. "map-icon-hospital") */ + poiImage?: string | null; + /** The `ImagePath` field from the API (fallback for icon resolution) */ + imagePath?: string | null; + /** The `Marker` field from the API (shape type: "MAP_PIN", "SHIELD", etc.) */ + marker?: string | null; + /** Display title for the marker */ + title: string; + /** Press handler */ + onPress?: () => void; +} + +/** Default fill color matching the web app. */ +export const DEFAULT_POI_COLOR = '#2563eb'; + +/** + * POI pin marker component that renders a colored SVG shape with a white icon inside. + * + * Matches the web application's POI marker rendering: + * - SVG background shape filled with the POI type's Color + * - White lucide icon centered on top of the shape + * - Drop shadow via an offset semi-transparent path + * - 36x48 pixel dimensions, bottom-center anchor + * + * This component is the SHAPE+ICON only (no title text). The title is rendered + * as a separate MarkerView in the parent (MapPins) so that the anchor measurement + * is not contaminated by overflow text. + */ +const PoiPinMarker: React.FC = ({ color, poiImage, imagePath, marker, title, onPress }) => { + const fillColor = color?.trim() || DEFAULT_POI_COLOR; + const shapePath = getPoiMarkerShapePath(marker); + const iconName = resolvePoiIcon(poiImage, imagePath); + + return ( + + {/* + * Shape background — kept in normal layout flow (NOT absolutely positioned) + * so that Mapbox.MarkerView measures the correct 36×48 dimensions when + * computing the anchor offset. Previously both children were absolute, + * giving the container an intrinsic height of 0 and causing the SVG to + * render entirely below the coordinate point ("too low / cut off"). + */} + + {/* Shadow layer: offset down by 1px, semi-transparent black */} + + {/* Main shape */} + + + {/* White icon — absolutely overlaid on the shape */} + + + + + ); +}; + +const styles = StyleSheet.create({ + /* + * No explicit width/height — the Svg in normal flow (36×48) determines the + * container size, which Mapbox can measure correctly for anchor computation. + */ + container: { + alignItems: 'center', + justifyContent: 'flex-start', + }, + iconContainer: { + position: 'absolute', + top: 12, + left: 0, + right: 0, + alignItems: 'center', + justifyContent: 'center', + }, +}); + +export default PoiPinMarker; diff --git a/src/components/ui/shared-tabs.tsx b/src/components/ui/shared-tabs.tsx index e303760..5f99247 100644 --- a/src/components/ui/shared-tabs.tsx +++ b/src/components/ui/shared-tabs.tsx @@ -197,10 +197,15 @@ export const SharedTabs: React.FC = ({ ) : ( {tab.title} )} + {/* + * Badge is absolutely positioned so it does NOT participate in the + * flex column layout (icon → text). Previously it was an inline + * third item that stacked below the label — the "2nd row" bug. + */} {tab.badge !== undefined && tab.badge > 0 && ( - - {tab.badge} - + + {tab.badge > 99 ? '99+' : tab.badge} + )} ))} @@ -229,4 +234,24 @@ const tabContentStyles = StyleSheet.create({ active: { flex: 1 }, // display:'none' hides the view and removes it from layout without unmounting hidden: { display: 'none' }, + // Badge overlay for the non-scrollable tab bar. Absolutely positioned so it + // sits in the top-right corner of the tab without pushing text to a 2nd row. + badge: { + position: 'absolute', + top: 2, + right: 2, + minWidth: 16, + height: 16, + borderRadius: 8, + backgroundColor: '#ef4444', + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 3, + }, + badgeText: { + fontSize: 9, + fontWeight: '700', + color: '#ffffff', + lineHeight: 11, + }, }); diff --git a/src/constants/map-icons.ts b/src/constants/map-icons.ts index 91651d4..8c8eaa5 100644 --- a/src/constants/map-icons.ts +++ b/src/constants/map-icons.ts @@ -225,16 +225,27 @@ export type MapIconKey = keyof typeof MAP_ICONS; const MAP_ICON_KEYS = new Set(Object.keys(MAP_ICONS)); +const stripMapIconPrefix = (value: string): string => { + if (value.toLowerCase().startsWith('map-icon-')) { + return value.slice(9); + } + return value; +}; + const normalizeMapIconToken = (value: string) => { - return value + // First strip "map-icon-" prefix if present, so "map-icon-hospital" + // normalizes to "hospital" instead of "mapiconhospital". + const stripped = stripMapIconPrefix(value); + + return stripped .trim() .toLowerCase() .replace(/\.[a-z0-9]+$/i, '') .replace(/[^a-z0-9]+/g, ''); }; -export const resolveMapIconKey = ({ imagePath, marker, fallback = 'call' }: { imagePath?: string | null; marker?: string | null; fallback?: MapIconKey }): MapIconKey => { - const tokens = [imagePath, marker].filter((value): value is string => typeof value === 'string' && value.trim().length > 0).map(normalizeMapIconToken); +export const resolveMapIconKey = ({ poiImage, imagePath, marker, fallback = 'call' }: { poiImage?: string | null; imagePath?: string | null; marker?: string | null; fallback?: MapIconKey }): MapIconKey => { + const tokens = [poiImage, imagePath, marker].filter((value): value is string => typeof value === 'string' && value.trim().length > 0).map(normalizeMapIconToken); for (const token of tokens) { if (MAP_ICON_KEYS.has(token)) { diff --git a/src/constants/poi-icon-mapping.ts b/src/constants/poi-icon-mapping.ts new file mode 100644 index 0000000..634fbbb --- /dev/null +++ b/src/constants/poi-icon-mapping.ts @@ -0,0 +1,200 @@ +/** + * POI icon mapping from map-icons CSS class names to lucide-react-native icon names. + * + * This mapping mirrors the map-icons font library used by the Resgrid web application. + * Each entry maps a map-icon CSS class name (e.g. "map-icon-hospital") to a + * lucide-react-native icon name (e.g. "hospital"). + * + * For mobile apps, lucide-react-native provides native vector icons that are + * rendered in white inside the colored SVG shape background. + */ + +/** + * Raw mapping: map-icon class name (without "map-icon-" prefix) → lucide icon name. + * All lucide icon names have been verified against lucide-react-native v0.475. + */ +const POI_ICON_TO_LUCIDE: Record = { + // Common POI types + 'map-pin': 'map-pin', + 'point-of-interest': 'map-pin', + hospital: 'hospital', + police: 'shield-alert', + 'fire-station': 'flame', + school: 'school', + bank: 'building', + 'post-office': 'mail', + church: 'church', + parking: 'circle-parking', + 'gas-station': 'fuel', + airport: 'plane', + restaurant: 'utensils', + 'grocery-or-supermarket': 'shopping-basket', + pharmacy: 'pill-bottle', + library: 'library', + museum: 'building-2', + stadium: 'building-2', + courthouse: 'gavel', + 'city-hall': 'building-2', + embassy: 'building-2', + campground: 'tent', + park: 'tree-pine', + lodging: 'hotel', + 'train-station': 'train-front', + 'bus-station': 'bus', + 'square-pin': 'map-pin', + shield: 'shield', + route: 'route', + square: 'square', + 'square-rounded': 'square', + + // Transportation + 'car-rental': 'car', + 'car-repair': 'wrench', + 'car-wash': 'car', + 'bicycle-store': 'bike', + bicycle: 'bike', + motorcycle: 'bike', + boat: 'ship', + ferry: 'ship', + 'subway-station': 'train-front', + 'taxi-stand': 'car', + + // Food & Drink + bakery: 'croissant', + bar: 'beer', + cafe: 'coffee', + 'convenience-store': 'store', + 'liquor-store': 'beer', + 'meal-delivery': 'bike', + 'meal-takeaway': 'shopping-basket', + supermarket: 'shopping-cart', + store: 'store', + + // Health + dentist: 'stethoscope', + doctor: 'stethoscope', + 'veterinary-care': 'stethoscope', + 'medical-store': 'cross', + + // Civic & Government + 'local-government-office': 'building-2', + 'local-post-office': 'mail', + 'place-of-worship': 'church', + mosque: 'church', + synagogue: 'church', + temple: 'church', + cemetery: 'church', + 'funeral-home': 'church', + 'fire-hydrant': 'flame', + 'police-box': 'shield-alert', + + // Education + university: 'graduation-cap', + college: 'graduation-cap', + daycare: 'baby', + 'primary-school': 'school', + 'secondary-school': 'school', + + // Recreation + aquarium: 'fish', + 'amusement-park': 'ferris-wheel', + 'art-gallery': 'palette', + beach: 'waves', + 'bowling-alley': 'circle', + casino: 'dices', + cinema: 'clapperboard', + 'movie-theater': 'clapperboard', + 'golf-course': 'flag', + gym: 'dumbbell', + 'hair-care': 'scissors', + 'night-club': 'music', + playground: 'tree-pine', + rink: 'circle', + spa: 'droplets', + 'tourist-information': 'info', + zoo: 'paw-print', + + // Business & Services + atm: 'landmark', + 'beauty-salon': 'scissors', + 'car-dealer': 'car', + 'clothing-store': 'shirt', + 'department-store': 'store', + 'electronics-store': 'monitor', + florist: 'flower', + 'furniture-store': 'armchair', + 'hardware-store': 'hammer', + 'home-goods-store': 'home', + 'insurance-agency': 'shield-check', + 'jewelry-store': 'gem', + laundromat: 'shirt', + lawyer: 'scale', + locksmith: 'key', + 'moving-company': 'truck', + 'painter-shop': 'paintbrush', + 'pet-store': 'paw-print', + plumber: 'wrench', + 'real-estate-agency': 'building', + 'roofing-contractor': 'home', + 'shoe-store': 'footprints', + 'shopping-mall': 'store', + 'storage-facility': 'warehouse', + 'travel-agency': 'globe', + + // Infrastructure + bridge: 'route', + dam: 'waves', + electrician: 'zap', + 'fire-alarm': 'bell', + 'fire-extinguisher': 'flame', + fountain: 'droplets', + gate: 'door-open', + lighthouse: 'tower', + memorial: 'flag', + monument: 'landmark', + 'mountain-pass': 'mountain', + 'natural-feature': 'mountain', + 'power-substation': 'zap', + 'power-tower': 'zap', + tower: 'antenna', + tunnel: 'arrow-down', + 'water-tower': 'droplets', + windmill: 'wind', +}; + +/** Default lucide icon name when a map-icon is not found in the mapping. */ +export const DEFAULT_POI_ICON = 'map-pin'; + +/** + * Strips the "map-icon-" prefix from a CSS class name to get the icon key. + * + * @param iconClass - The full CSS class name (e.g. "map-icon-hospital") + * @returns The icon key without the prefix (e.g. "hospital"), or the original string + * if it doesn't start with "map-icon-". + */ +const stripMapIconPrefix = (iconClass: string): string => { + if (iconClass.toLowerCase().startsWith('map-icon-')) { + return iconClass.slice(9); // Remove "map-icon-" + } + return iconClass; +}; + +/** + * Resolves a map-icon CSS class name to a lucide-react-native icon name. + * + * @param poiImage - The `PoiImage` field value from the API (e.g. "map-icon-hospital") + * @param imagePath - The `ImagePath` field as fallback + * @returns A lucide-react-native icon name string, or the default if not found. + */ +export const resolvePoiIcon = (poiImage?: string | null, imagePath?: string | null): string => { + const tokens = [poiImage, imagePath].filter((v): v is string => typeof v === 'string' && v.trim().length > 0); + + for (const token of tokens) { + const key = stripMapIconPrefix(token.trim()).toLowerCase(); + if (POI_ICON_TO_LUCIDE[key]) { + return POI_ICON_TO_LUCIDE[key]; + } + } + + return DEFAULT_POI_ICON; +}; diff --git a/src/constants/poi-marker-shapes.ts b/src/constants/poi-marker-shapes.ts new file mode 100644 index 0000000..70361ed --- /dev/null +++ b/src/constants/poi-marker-shapes.ts @@ -0,0 +1,91 @@ +/** + * SVG path definitions for POI marker background shapes. + * All shapes use the viewBox="-24 -48 48 48" coordinate system as defined in the web app. + * + * Origin (0,0) is at the top-center of the 48×48 bounding box. + * X goes from -24 (left) to +24 (right). + * Y goes from -48 (top) to 0 (bottom). + * The tip of the pin is at (0, 0) (bottom-center). + * + * When rendered at 36×48 pixels: + * scaleX = 36/48 = 0.75 + * scaleY = 48/48 = 1.0 + * offsetX = 24 * 0.75 = 18 + * offsetY = 48 * 1.0 = 48 + */ + +export type PoiMarkerShape = 'MAP_PIN' | 'SHIELD' | 'ROUTE' | 'SQUARE' | 'SQUARE_ROUNDED'; + +/** + * Classic teardrop-shaped map pin. + */ +const MAP_PIN_PATH = 'M0-48c-9.8 0-17.7 7.8-17.7 17.4 0 15.5 17.7 30.6 17.7 30.6s17.7-15.4 17.7-30.6c0-9.6-7.9-17.4-17.7-17.4z'; + +/** + * Crest/shield shape. + */ +const SHIELD_PATH = + 'M18.8-31.8c.3-3.4 1.3-6.6 3.2-9.5l-7-6.7c-2.2 1.8-4.8 2.8-7.6 3-2.6.2-5.1-.2-7.5-1.4-2.4 1.1-4.9 1.6-7.5 1.4-2.7-.2-5.1-1.1-7.3-2.7l-7.1 6.7c1.7 2.9 2.7 6 2.9 9.2.1 1.5-.3 3.5-1.3 6.1-.5 1.5-.9 2.7-1.2 3.8-.2 1-.4 1.9-.5 2.5 0 2.8.8 5.3 2.5 7.5 1.3 1.6 3.5 3.4 6.5 5.4 3.3 1.6 5.8 2.6 7.6 3.1.5.2 1 .4 1.5.7l1.5.6c1.2.7 2 1.4 2.4 2.1.5-.8 1.3-1.5 2.4-2.1.7-.3 1.3-.5 1.9-.8.5-.2.9-.4 1.1-.5.4-.1.9-.3 1.5-.6.6-.2 1.3-.5 2.2-.8 1.7-.6 3-1.1 3.8-1.6 2.9-2 5.1-3.8 6.4-5.3 1.7-2.2 2.6-4.8 2.5-7.6-.1-1.3-.7-3.3-1.7-6.1-.9-2.8-1.3-4.9-1.2-6.4z'; + +/** + * Route sign shape. + */ +const ROUTE_PATH = + 'M24-28.3c-.2-13.3-7.9-18.5-8.3-18.7l-1.2-.8-1.2.8c-2 1.4-4.1 2-6.1 2-3.4 0-5.8-1.9-5.9-1.9l-1.3-1.1-1.3 1.1c-.1.1-2.5 1.9-5.9 1.9-2.1 0-4.1-.7-6.1-2l-1.2-.8-1.2.8c-.8.6-8 5.9-8.2 18.7-.2 1.1 2.9 22.2 23.9 28.3 22.9-6.7 24.1-26.9 24-28.3z'; + +/** + * Plain square (no rounded corners). + * Note: SVG viewBox is -24,-48,48,48, but for SQUARE we use a simpler path. + */ +const SQUARE_PATH = 'M-24-48h48v48h-48z'; + +/** + * Square with rounded corners. + */ +const SQUARE_ROUNDED_PATH = 'M24-8c0 4.4-3.6 8-8 8h-32c-4.4 0-8-3.6-8-8v-32c0-4.4 3.6-8 8-8h32c4.4 0 8 3.6 8 8v32z'; + +/** + * Lookup table for all supported POI marker shapes. + * The key is the normalized (uppercase) shape name as provided by the API's `Marker` field. + */ +export const POI_MARKER_PATHS: Record = { + MAP_PIN: MAP_PIN_PATH, + SHIELD: SHIELD_PATH, + ROUTE: ROUTE_PATH, + SQUARE: SQUARE_PATH, + SQUARE_ROUNDED: SQUARE_ROUNDED_PATH, +} as const; + +/** Default shape when none is specified or the shape is unknown. */ +export const DEFAULT_POI_MARKER_SHAPE: PoiMarkerShape = 'MAP_PIN'; + +/** + * Returns the SVG path data for a given POI marker shape. + * + * @param markerShape - The `Marker` field value from the API (e.g. "MAP_PIN", "SHIELD"). + * Normalized to uppercase internally. Null/empty values default to MAP_PIN. + * @returns The SVG path `d` attribute string. + */ +export const getPoiMarkerShapePath = (markerShape?: string | null): string => { + const normalized = (markerShape ?? '').trim().toUpperCase(); + + if (normalized.length === 0) { + return MAP_PIN_PATH; + } + + return POI_MARKER_PATHS[normalized] ?? MAP_PIN_PATH; +}; + +/** SVG viewBox used for all shapes. */ +export const POI_MARKER_VIEWBOX = '-24 -48 48 48'; + +/** The aspect ratio of the SVG shapes (width/height). */ +export const POI_MARKER_ASPECT_RATIO = 48 / 48; // 1.0 + +/** Total marker dimensions in pixels (matching the web app). */ +export const POI_MARKER_WIDTH = 36; +export const POI_MARKER_HEIGHT = 48; + +/** Anchor point for the marker (bottom-center). */ +export const POI_MARKER_ANCHOR_X = 0.5; +export const POI_MARKER_ANCHOR_Y = 1.0; diff --git a/src/lib/logging/index.tsx b/src/lib/logging/index.tsx index c0351b8..d6aaae1 100644 --- a/src/lib/logging/index.tsx +++ b/src/lib/logging/index.tsx @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/react-native'; import { consoleTransport, logger as rnLogger } from 'react-native-logs'; import type { LogEntry, Logger, LogLevel } from './types'; @@ -49,6 +50,22 @@ class LogService { ...context, timestamp: new Date().toISOString(), }); + + // Add log breadcrumb to Sentry for error/warn levels + // Provides rich debugging context when errors occur + if (level === 'error' || level === 'warn') { + try { + Sentry.addBreadcrumb({ + category: 'log', + level: level === 'error' ? 'error' : 'warning', + message: String(message), + data: { ...this.globalContext, ...context }, + timestamp: Date.now() / 1000, // Unix timestamp in seconds + }); + } catch { + // Sentry may not be initialized yet — silently ignore + } + } } public setGlobalContext(context: Record): void { diff --git a/src/lib/poi.ts b/src/lib/poi.ts index b02f5ca..b164cbd 100644 --- a/src/lib/poi.ts +++ b/src/lib/poi.ts @@ -1,6 +1,52 @@ +import { type MapMakerInfoData } from '@/models/v4/mapping/getMapDataAndMarkersData'; import { type PoiResultData } from '@/models/v4/mapping/poiResultData'; import { type PoiTypeResultData } from '@/models/v4/mapping/poiTypeResultData'; +//region POI Marker Detection + +export const POI_MARKER_TYPE = 4; + +/** + * Determines whether a map marker represents a Point of Interest (POI). + * + * Matches the web application's `isPoiMarker()` logic: + * 1. marker.Type === 4 (explicit POI type) + * 2. marker.PoiTypeId is a number greater than 0 + * 3. marker.LayerId starts with the string "poi-type-" + * 4. marker.PoiImage (or ImagePath) starts with "map-icon-" (case-insensitive) + * + * @param marker - A map marker from the API response. + * @returns true if the marker is a POI, false otherwise. + */ +export const isPoiMarker = (marker: Pick): boolean => { + // 1. Explicit POI type + if (marker.Type === POI_MARKER_TYPE) { + return true; + } + + // 2. Has a POI type ID greater than 0 + if (marker.PoiTypeId != null && marker.PoiTypeId > 0) { + return true; + } + + // 3. Layer ID starts with "poi-type-" + if (marker.LayerId != null && marker.LayerId.toLowerCase().startsWith('poi-type-')) { + return true; + } + + // 4. PoiImage or ImagePath starts with "map-icon-" (case-insensitive) + const iconField = marker.PoiImage ?? marker.ImagePath; + if (iconField != null && iconField.toLowerCase().startsWith('map-icon-')) { + return true; + } + + return false; +}; + +//endregion + +//region POI Display & Grouping + export interface PoiDisplayable { Name?: string | null; Address?: string | null; @@ -61,7 +107,7 @@ export const groupPoisByType = (pois: PoiResultData[], poiTypes: PoiTypeResultDa poiTypeName: poi.PoiTypeName || poiType?.Name || '', isDestination: poi.IsDestination || poiType?.IsDestination || false, color: poi.Color || poiType?.Color || '', - imagePath: poi.ImagePath || poiType?.ImagePath || '', + imagePath: poi.PoiImage || poi.ImagePath || poiType?.PoiImage || poiType?.ImagePath || '', marker: poi.Marker || poiType?.Marker || '', pois: [poi], }); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3080f8f..8a5f631 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,7 +2,7 @@ import { Linking } from 'react-native'; import { Platform } from 'react-native'; import type { StoreApi, UseBoundStore } from 'zustand'; -import { Env } from './env'; +import { getBaseApiUrl } from '@/lib/storage/app'; export function openLinkInBrowser(url: string) { Linking.canOpenURL(url).then((canOpen) => canOpen && Linking.openURL(url)); @@ -54,7 +54,7 @@ export function onSortOptions(a: any, b: any) { } export function getAvatarUrl(userId: string) { - return Env.BASE_API_URL + Env.RESGRID_API_URL + '/Avatars/Get?id=' + userId; + return getBaseApiUrl() + '/Avatars/Get?id=' + userId; } export function invertColor(hex: string, bw: boolean): string { diff --git a/src/models/v4/mapping/getMapDataAndMarkersData.ts b/src/models/v4/mapping/getMapDataAndMarkersData.ts index 5ccb770..304d7e7 100644 --- a/src/models/v4/mapping/getMapDataAndMarkersData.ts +++ b/src/models/v4/mapping/getMapDataAndMarkersData.ts @@ -18,6 +18,7 @@ export class MapMakerInfoData { public InfoWindowContent: string = ''; public Color: string = ''; public Type: number = 0; + public PoiImage?: string; public Marker?: string; public PoiTypeId?: number | null; public PoiTypeName?: string; diff --git a/src/models/v4/mapping/poiLayerData.ts b/src/models/v4/mapping/poiLayerData.ts index ae72c3b..89f533e 100644 --- a/src/models/v4/mapping/poiLayerData.ts +++ b/src/models/v4/mapping/poiLayerData.ts @@ -3,6 +3,7 @@ export class PoiLayerData { public Name: string = ''; public Color: string = ''; public ImagePath: string = ''; + public PoiImage: string = ''; public Marker: string = ''; public IsDestination: boolean = false; } diff --git a/src/models/v4/mapping/poiResultData.ts b/src/models/v4/mapping/poiResultData.ts index 2dad659..c74549c 100644 --- a/src/models/v4/mapping/poiResultData.ts +++ b/src/models/v4/mapping/poiResultData.ts @@ -9,6 +9,7 @@ export class PoiResultData { public Longitude: number = 0; public Color: string = ''; public ImagePath: string = ''; + public PoiImage: string = ''; public Marker: string = ''; public IsDestination: boolean = false; } diff --git a/src/models/v4/mapping/poiTypeResultData.ts b/src/models/v4/mapping/poiTypeResultData.ts index e26082d..23fab04 100644 --- a/src/models/v4/mapping/poiTypeResultData.ts +++ b/src/models/v4/mapping/poiTypeResultData.ts @@ -3,6 +3,7 @@ export class PoiTypeResultData { public Name: string = ''; public Color: string = ''; public ImagePath: string = ''; + public PoiImage: string = ''; public Marker: string = ''; public IsDestination: boolean = false; } diff --git a/src/translations/ar.json b/src/translations/ar.json index 93ece3b..e158117 100644 --- a/src/translations/ar.json +++ b/src/translations/ar.json @@ -838,6 +838,7 @@ "empty_title": "لم يتم العثور على نقاط اهتمام", "filter_all_types": "كل الأنواع", "filter_label": "تصفية", + "filter_sort_title": "فلترة وفرز", "invalid_description": "مطلوب معرّف نقطة اهتمام لعرض التفاصيل.", "invalid_title": "نقطة اهتمام غير صالحة", "list_description": "تصفح نقاط الاهتمام الخاصة بالقسم، وقم بالتصفية حسب النوع، ثم ارجع إلى الخريطة.", @@ -846,9 +847,11 @@ "loading_detail": "جارٍ تحميل تفاصيل نقطة الاهتمام...", "not_found": "تعذر العثور على نقطة الاهتمام", "note_label": "ملاحظة", + "reset_filters": "إعادة تعيين", "results_count": "{{count}} نقطة اهتمام", "results_count_plural": "{{count}} نقاط اهتمام", "route_error": "تعذر فتح تطبيق الخرائط", + "search_placeholder": "البحث في نقاط الاهتمام...", "set_status_destination": "استخدامها كوجهة للحالة", "sort_label": "ترتيب", "sort_name": "الاسم", diff --git a/src/translations/de.json b/src/translations/de.json index 7056af5..dc0cda7 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -838,6 +838,7 @@ "empty_title": "Keine POIs gefunden", "filter_all_types": "Alle Typen", "filter_label": "Filter", + "filter_sort_title": "Filter & Sortierung", "invalid_description": "Zum Anzeigen der POI-Details ist eine POI-ID erforderlich.", "invalid_title": "Ungültiger POI", "list_description": "Durchsuchen Sie die Abteilungs-POIs, filtern Sie nach Typ und kehren Sie zur Karte zurück.", @@ -846,9 +847,11 @@ "loading_detail": "Lade POI-Details...", "not_found": "POI nicht gefunden", "note_label": "Hinweis", + "reset_filters": "Zurücksetzen", "results_count": "{{count}} POI", "results_count_plural": "{{count}} POIs", "route_error": "Karten-App konnte nicht geöffnet werden", + "search_placeholder": "POIs durchsuchen...", "set_status_destination": "Als Statusziel verwenden", "sort_label": "Sortieren", "sort_name": "Name", diff --git a/src/translations/en.json b/src/translations/en.json index 3cf038d..9fafd0b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -838,6 +838,7 @@ "empty_title": "No POIs found", "filter_all_types": "All types", "filter_label": "Filter", + "filter_sort_title": "Filter & Sort", "invalid_description": "A POI id is required to view POI details.", "invalid_title": "Invalid POI", "list_description": "Browse department POIs, filter by type, and jump back to the map.", @@ -846,9 +847,11 @@ "loading_detail": "Loading POI details...", "not_found": "POI not found", "note_label": "Note", + "reset_filters": "Reset", "results_count": "{{count}} POI", "results_count_plural": "{{count}} POIs", "route_error": "Failed to open maps application", + "search_placeholder": "Search POIs...", "set_status_destination": "Set status destination", "sort_label": "Sort", "sort_name": "Name", diff --git a/src/translations/es.json b/src/translations/es.json index f056a30..c6f6db2 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -838,6 +838,7 @@ "empty_title": "No se encontraron POI", "filter_all_types": "Todos los tipos", "filter_label": "Filtro", + "filter_sort_title": "Filtrar y ordenar", "invalid_description": "Se requiere un identificador de POI para ver los detalles.", "invalid_title": "POI no válido", "list_description": "Explora los POI del departamento, filtra por tipo y vuelve al mapa.", @@ -846,9 +847,11 @@ "loading_detail": "Cargando detalles del POI...", "not_found": "POI no encontrado", "note_label": "Nota", + "reset_filters": "Restablecer", "results_count": "{{count}} POI", "results_count_plural": "{{count}} POIs", "route_error": "No se pudo abrir la aplicación de mapas", + "search_placeholder": "Buscar POI...", "set_status_destination": "Usar como destino del estado", "sort_label": "Ordenar", "sort_name": "Nombre", diff --git a/src/translations/fr.json b/src/translations/fr.json index 3090cd9..11d2dd0 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -838,6 +838,7 @@ "empty_title": "Aucun POI trouvé", "filter_all_types": "Tous les types", "filter_label": "Filtre", + "filter_sort_title": "Filtrer et trier", "invalid_description": "Un identifiant de POI est requis pour afficher les détails.", "invalid_title": "POI invalide", "list_description": "Parcourez les POI du service, filtrez par type et revenez à la carte.", @@ -846,9 +847,11 @@ "loading_detail": "Chargement des détails du POI...", "not_found": "POI introuvable", "note_label": "Remarque", + "reset_filters": "Réinitialiser", "results_count": "{{count}} POI", "results_count_plural": "{{count}} POIs", "route_error": "Impossible d'ouvrir l'application de cartographie", + "search_placeholder": "Rechercher des POI...", "set_status_destination": "Utiliser comme destination du statut", "sort_label": "Trier", "sort_name": "Nom", diff --git a/src/translations/it.json b/src/translations/it.json index 59e8b5f..d110cb6 100644 --- a/src/translations/it.json +++ b/src/translations/it.json @@ -838,6 +838,7 @@ "empty_title": "Nessun POI trovato", "filter_all_types": "Tutti i tipi", "filter_label": "Filtro", + "filter_sort_title": "Filtra e ordina", "invalid_description": "Per visualizzare i dettagli del POI è necessario un ID POI.", "invalid_title": "POI non valido", "list_description": "Sfoglia i POI del dipartimento, filtra per tipo e torna alla mappa.", @@ -846,9 +847,11 @@ "loading_detail": "Caricamento dettagli POI...", "not_found": "POI non trovato", "note_label": "Nota", + "reset_filters": "Reimposta", "results_count": "{{count}} POI", "results_count_plural": "{{count}} POIs", "route_error": "Impossibile aprire l'app di mappe", + "search_placeholder": "Cerca POI...", "set_status_destination": "Usa come destinazione dello stato", "sort_label": "Ordina", "sort_name": "Nome", diff --git a/src/translations/pl.json b/src/translations/pl.json index 42f0187..9280c29 100644 --- a/src/translations/pl.json +++ b/src/translations/pl.json @@ -838,6 +838,7 @@ "empty_title": "Nie znaleziono POI", "filter_all_types": "Wszystkie typy", "filter_label": "Filtr", + "filter_sort_title": "Filtruj i sortuj", "invalid_description": "Aby wyświetlić szczegóły POI, wymagany jest identyfikator POI.", "invalid_title": "Nieprawidłowy POI", "list_description": "Przeglądaj POI działu, filtruj według typu i wróć do mapy.", @@ -846,9 +847,11 @@ "loading_detail": "Ładowanie szczegółów POI...", "not_found": "Nie znaleziono POI", "note_label": "Notatka", + "reset_filters": "Resetuj", "results_count": "{{count}} POI", "results_count_plural": "{{count}} POIs", "route_error": "Nie udało się otworzyć aplikacji map", + "search_placeholder": "Szukaj POI...", "set_status_destination": "Użyj jako celu statusu", "sort_label": "Sortuj", "sort_name": "Nazwa", diff --git a/src/translations/sv.json b/src/translations/sv.json index 6097bfc..c16216c 100644 --- a/src/translations/sv.json +++ b/src/translations/sv.json @@ -838,6 +838,7 @@ "empty_title": "Inga POI:er hittades", "filter_all_types": "Alla typer", "filter_label": "Filter", + "filter_sort_title": "Filtrera och sortera", "invalid_description": "Ett POI-id krävs för att visa POI-detaljer.", "invalid_title": "Ogiltig POI", "list_description": "Bläddra bland avdelningens POI:er, filtrera efter typ och hoppa tillbaka till kartan.", @@ -846,9 +847,11 @@ "loading_detail": "Laddar POI-detaljer...", "not_found": "POI hittades inte", "note_label": "Anteckning", + "reset_filters": "Återställ", "results_count": "{{count}} POI", "results_count_plural": "{{count}} POIs", "route_error": "Det gick inte att öppna kartappen", + "search_placeholder": "Sök POI:er...", "set_status_destination": "Använd som statusdestination", "sort_label": "Sortera", "sort_name": "Namn", diff --git a/src/translations/uk.json b/src/translations/uk.json index 516a173..3becefb 100644 --- a/src/translations/uk.json +++ b/src/translations/uk.json @@ -838,6 +838,7 @@ "empty_title": "POI не знайдено", "filter_all_types": "Усі типи", "filter_label": "Фільтр", + "filter_sort_title": "Фільтр та сортування", "invalid_description": "Щоб переглянути деталі POI, потрібен ідентифікатор POI.", "invalid_title": "Недійсний POI", "list_description": "Переглядайте POI підрозділу, фільтруйте за типом і повертайтеся до карти.", @@ -846,9 +847,11 @@ "loading_detail": "Завантаження деталей POI...", "not_found": "POI не знайдено", "note_label": "Примітка", + "reset_filters": "Скинути", "results_count": "{{count}} POI", "results_count_plural": "{{count}} POIs", "route_error": "Не вдалося відкрити програму карт", + "search_placeholder": "Пошук POI...", "set_status_destination": "Використати як пункт призначення статусу", "sort_label": "Сортувати", "sort_name": "Назва", diff --git a/yarn.lock b/yarn.lock index fd3f259..aad1d37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4101,144 +4101,150 @@ micromatch "^4.0.0" p-reduce "^2.0.0" -"@sentry-internal/browser-utils@10.12.0": - version "10.12.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-10.12.0.tgz#aa3a05653e530d2693e307c0131571ee8a97b60d" - integrity sha512-dozbx389jhKynj0d657FsgbBVOar7pX3mb6GjqCxslXF0VKpZH2Xks0U32RgDY/nK27O+o095IWz7YvjVmPkDw== +"@sentry-internal/browser-utils@10.51.0": + version "10.51.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-10.51.0.tgz#03f36703f13a9af7b593e24d0e9ab92aa6a0b601" + integrity sha512-lNKBS4P7RUvf1niojXQWe9bU3gnBUCbST4Dj0pSiyat1N96cXVyHkeE+uGxowD0RrVWhs+kGHiVX3FcmRWF6sA== dependencies: - "@sentry/core" "10.12.0" + "@sentry/core" "10.51.0" -"@sentry-internal/feedback@10.12.0": - version "10.12.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-10.12.0.tgz#a48039507f37fe62e19566128a894661a724ef0d" - integrity sha512-0+7ceO6yQPPqfxRc9ue/xoPHKcnB917ezPaehGQNfAFNQB9PNTG1y55+8mRu0Fw+ANbZeCt/HyoCmXuRdxmkpg== +"@sentry-internal/feedback@10.51.0": + version "10.51.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-10.51.0.tgz#2119ee63d06a1f1efe0017e60c5718f995192c61" + integrity sha512-bCM95bcpphx28e6aU0bwRLxOgwosYsdNzezM1sM0pVOkb0TB3hDFRamramVDK+/Hp1o8qmRxS4c5w/A7YBZGkA== dependencies: - "@sentry/core" "10.12.0" + "@sentry/core" "10.51.0" -"@sentry-internal/replay-canvas@10.12.0": - version "10.12.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-10.12.0.tgz#f79dde92bcba67b4f706db6c217467e14d6348c5" - integrity sha512-W/z1/+69i3INNfPjD1KuinSNaRQaApjzwb37IFmiyF440F93hxmEYgXHk3poOlYYaigl2JMYbysGPWOiXnqUXA== +"@sentry-internal/replay-canvas@10.51.0": + version "10.51.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-10.51.0.tgz#f20fc2703e155778562e42fcbfd1bce43e9adba2" + integrity sha512-8PW1Pp+Yl3lPwYqhBCr5SgkuhDanu9ZLzUqD2bPKL/ElqbM2eDVIWxq4z4ZzePrmZa6IcCjTv6sVQJ7Z4dLyLA== dependencies: - "@sentry-internal/replay" "10.12.0" - "@sentry/core" "10.12.0" + "@sentry-internal/replay" "10.51.0" + "@sentry/core" "10.51.0" -"@sentry-internal/replay@10.12.0": - version "10.12.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-10.12.0.tgz#47ca89acdc621217991c7ed1d133fd37915a512d" - integrity sha512-/1093gSNGN5KlOBsuyAl33JkzGiG38kCnxswQLZWpPpR6LBbR1Ddb18HjhDpoQNNEZybJBgJC3a5NKl43C2TSQ== +"@sentry-internal/replay@10.51.0": + version "10.51.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-10.51.0.tgz#2b2153104066de466b55a682b900ea31e60b11a0" + integrity sha512-jCpI5HXSwK6ZT2HX70+mDRciAocHzSiDk4DTgvzV69Wvd+Ei5WLgE+d39eaEPsm8lUC0Ydntb5sJIB6uG9D4bw== dependencies: - "@sentry-internal/browser-utils" "10.12.0" - "@sentry/core" "10.12.0" - -"@sentry/babel-plugin-component-annotate@4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.3.0.tgz#c5b6cbb986952596d3ad233540a90a1fd18bad80" - integrity sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw== + "@sentry-internal/browser-utils" "10.51.0" + "@sentry/core" "10.51.0" -"@sentry/browser@10.12.0": - version "10.12.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-10.12.0.tgz#79dffc88e1f9241b9fdb5def5a7c6809f41230b3" - integrity sha512-lKyaB2NFmr7SxPjmMTLLhQ7xfxaY3kdkMhpzuRI5qwOngtKt4+FtvNYHRuz+PTtEFv4OaHhNNbRn6r91gWguQg== - dependencies: - "@sentry-internal/browser-utils" "10.12.0" - "@sentry-internal/feedback" "10.12.0" - "@sentry-internal/replay" "10.12.0" - "@sentry-internal/replay-canvas" "10.12.0" - "@sentry/core" "10.12.0" - -"@sentry/cli-darwin@2.55.0": - version "2.55.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.55.0.tgz#79513547d15223d51905e94d8c1e2bc7377cfdf1" - integrity sha512-jGHE7SHHzqXUmnsmRLgorVH6nmMmTjQQXdPZbSL5tRtH8d3OIYrVNr5D72DSgD26XAPBDMV0ibqOQ9NKoiSpfA== - -"@sentry/cli-linux-arm64@2.55.0": - version "2.55.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.55.0.tgz#5a2e8ea6e088f4884cbe272525f0781f8c748ff3" - integrity sha512-jNB/0/gFcOuDCaY/TqeuEpsy/k52dwyk1SOV3s1ku4DUsln6govTppeAGRewY3T1Rj9B2vgIWTrnB8KVh9+Rgg== - -"@sentry/cli-linux-arm@2.55.0": - version "2.55.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.55.0.tgz#54c330471c4b23ff6769bfd886f092afba3380ce" - integrity sha512-ATjU0PsiWADSPLF/kZroLZ7FPKd5W9TDWHVkKNwIUNTei702LFgTjNeRwOIzTgSvG3yTmVEqtwFQfFN/7hnVXQ== - -"@sentry/cli-linux-i686@2.55.0": - version "2.55.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.55.0.tgz#12c64453ef7014ba89c885497e3c9764a5f3e0a0" - integrity sha512-8LZjo6PncTM6bWdaggscNOi5r7F/fqRREsCwvd51dcjGj7Kp1plqo9feEzYQ+jq+KUzVCiWfHrUjddFmYyZJrg== - -"@sentry/cli-linux-x64@2.55.0": - version "2.55.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.55.0.tgz#c640464fe533fe87e57d0ca5585e0c504e0ef5b3" - integrity sha512-5LUVvq74Yj2cZZy5g5o/54dcWEaX4rf3myTHy73AKhRj1PABtOkfexOLbF9xSrZy95WXWaXyeH+k5n5z/vtHfA== - -"@sentry/cli-win32-arm64@2.55.0": - version "2.55.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.55.0.tgz#da1e8ba13083c281fcdf72d1dd35255df7fdb5c5" - integrity sha512-cWIQdzm1pfLwPARsV6dUb8TVd6Y3V1A2VWxjTons3Ift6GvtVmiAe0OWL8t2Yt95i8v61kTD/6Tq21OAaogqzA== - -"@sentry/cli-win32-i686@2.55.0": - version "2.55.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.55.0.tgz#c2eae0a75fc55f101c31de0999214d7e613d65c4" - integrity sha512-ldepCn2t9r4I0wvgk7NRaA7coJyy4rTQAzM66u9j5nTEsUldf66xym6esd5ZZRAaJUjffqvHqUIr/lrieTIrVg== - -"@sentry/cli-win32-x64@2.55.0": - version "2.55.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.55.0.tgz#499e6697663a3e1453ff2e921bf3876f774520b5" - integrity sha512-4hPc/I/9tXx+HLTdTGwlagtAfDSIa2AoTUP30tl32NAYQhx9a6niUbPAemK2qfxesiufJ7D2djX83rCw6WnJVA== - -"@sentry/cli@2.55.0": - version "2.55.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.55.0.tgz#c22d2edbd320242e7881c15ecf89649378c05387" - integrity sha512-cynvcIM2xL8ddwELyFRSpZQw4UtFZzoM2rId2l9vg7+wDREPDocMJB9lEQpBIo3eqhp9JswqUT037yjO6iJ5Sw== - dependencies: - https-proxy-agent "^5.0.0" - node-fetch "^2.6.7" +"@sentry/babel-plugin-component-annotate@5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-5.2.1.tgz#99ab8cc4e5482d9b1ad3d09671ce999c59f40bae" + integrity sha512-QQ9AL5EXIbSK26ObLVtiU6l3tCUdpGSJ/6VwDkPhC3qvtoksSlcoU9Yzm7XC0NBcvu1N2abL5R7gckKGZ4JewQ== + +"@sentry/browser@10.51.0": + version "10.51.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-10.51.0.tgz#d5ba0215b48651ad4f07c9865c8df04e517c584e" + integrity sha512-Zdc0sKfenxUtW/OGhtJ7xHFN44bXR7YqxJ1zBDzlZfW0nTbeTTUZBq9z5NUw6qdS0Vs/i3V4qzAKTbRKWfqSEA== + dependencies: + "@sentry-internal/browser-utils" "10.51.0" + "@sentry-internal/feedback" "10.51.0" + "@sentry-internal/replay" "10.51.0" + "@sentry-internal/replay-canvas" "10.51.0" + "@sentry/core" "10.51.0" + +"@sentry/cli-darwin@3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-3.4.1.tgz#71d7daed5d87f86ba479c3fbdafeb280bb22b909" + integrity sha512-44foor4g/nfFaOaEZYQnxBrAW7TOMO4LatYsRjPI8dAoqXNVsl+P77FIk0gGAFnTwbt5gREXeeOn6j8DA9NyZg== + +"@sentry/cli-linux-arm64@3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.4.1.tgz#bd75c2b4f50faa3faa65e4de996c982b1c01af95" + integrity sha512-rYWeBxVEiYMZ5hUe16qkpCmJQBc+lxT50sls/CqO5WTN3VlrSRlJsd+jMTKUNesM0j4PMEi82Xy7rovD8a+2tA== + +"@sentry/cli-linux-arm@3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-3.4.1.tgz#c1911c632d82ee3bd7a4ffae32497a087d6407e3" + integrity sha512-XIT4ICA86vwrZWfbvKRiY9HgMg1aJNv1VJgNuiWu/3ysk0H4U7U4rJl6SQNbthgNGpcxvFdXmHbujKv7VZdv5w== + +"@sentry/cli-linux-i686@3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-3.4.1.tgz#56532d100037ab6806aa658a59fcb79061dddf50" + integrity sha512-yirtGGummlarcrPmIm4cg5vEA5gYnL/GJ6FLV6yfq1zAeMzEWnl7kaWIVsoTZ8qBi7vmHpy0APlH5LXxK4QXNw== + +"@sentry/cli-linux-x64@3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-3.4.1.tgz#8df916f85d6def62ba2b42bdf5cb86621dedca05" + integrity sha512-r2wJq9Bt1eRFtnAo3vWACfAN3IdV5gRfLnH8rbtDsg7pqdM0MP7tgAjzJDLptLGP02ndPhJfeJ1mXjcDY1MqqQ== + +"@sentry/cli-win32-arm64@3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.4.1.tgz#793b7ee9f4ab12d6ba575199b3fa56341279b419" + integrity sha512-IWg2FeB9OzxDky3q885MqepN2QEujyGdbcCB0VbHB3zRpT2D2fbk87FIy64JGoZkPVmoI4d7Mbxa+XKFS4jlrw== + +"@sentry/cli-win32-i686@3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-3.4.1.tgz#8049f96619a33b645242ef73cc792ce04e57ac95" + integrity sha512-RlKYU1Cdyk0uqRa9llKA6yVxg2QK0CXX0bogv89lIfABgZ4o1o45zcwVmEQLrD5rrIQmUcIJHWysQVnSyydJkA== + +"@sentry/cli-win32-x64@3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-3.4.1.tgz#b17eeddb4dd92015c89400dace0a657e143966a1" + integrity sha512-MgKSglGeD+zOIEdbZrt+P9v9ExkMrHKwlIK8hnJA8qNVjmPuOW9yR4khzdVIYd3cdujAQQhLcV3gEB9ceR4PxQ== + +"@sentry/cli@3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-3.4.1.tgz#a1336010e8af55a613ed80dc5030f38bb012de11" + integrity sha512-xY9WcIg+/B/bJxY1KbP0XrDZGPQaFNjIOVJbXNbwVtRujiG6DEwVHqGJhADjPa8rilLQVDOUumVMwLbAUCmh6A== + dependencies: progress "^2.0.3" proxy-from-env "^1.1.0" + undici "^6.22.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.55.0" - "@sentry/cli-linux-arm" "2.55.0" - "@sentry/cli-linux-arm64" "2.55.0" - "@sentry/cli-linux-i686" "2.55.0" - "@sentry/cli-linux-x64" "2.55.0" - "@sentry/cli-win32-arm64" "2.55.0" - "@sentry/cli-win32-i686" "2.55.0" - "@sentry/cli-win32-x64" "2.55.0" - -"@sentry/core@10.12.0": - version "10.12.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-10.12.0.tgz#3f6a0f5c2f63f2c1761e3cf442a986d74adf6403" - integrity sha512-Jrf0Yo7DvmI/ZQcvBnA0xKNAFkJlVC/fMlvcin+5IrFNRcqOToZ2vtF+XqTgjRZymXQNE8s1QTD7IomPHk0TAw== - -"@sentry/react-native@~7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-7.2.0.tgz#d29deacc36fd55fc52119ffcf53f61c21cab3600" - integrity sha512-rjqYgEjntPz1sPysud78wi4B9ui7LBVPsG6qr8s/htLMYho9GPGFA5dF+eqsQWqMX8NDReAxNkLTC4+gCNklLQ== - dependencies: - "@sentry/babel-plugin-component-annotate" "4.3.0" - "@sentry/browser" "10.12.0" - "@sentry/cli" "2.55.0" - "@sentry/core" "10.12.0" - "@sentry/react" "10.12.0" - "@sentry/types" "10.12.0" - -"@sentry/react@10.12.0": - version "10.12.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-10.12.0.tgz#c121f37bf582f4851108f67ef492de6a4c8f7a8d" - integrity sha512-TpqgdoYbkf5JynmmW2oQhHQ/h5w+XPYk0cEb/UrsGlvJvnBSR+5tgh0AqxCSi3gvtp82rAXI5w1TyRPBbhLDBw== - dependencies: - "@sentry/browser" "10.12.0" - "@sentry/core" "10.12.0" - hoist-non-react-statics "^3.3.2" - -"@sentry/types@10.12.0": - version "10.12.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-10.12.0.tgz#86d24346efde7b0757474537af7ae4b3d931d6a8" - integrity sha512-sKGj3l3V8ZKISh2Tu88bHfnm5ztkRtSLdmpZ6TmCeJdSM9pV+RRd6CMJ0RnSEXmYHselPNUod521t2NQFd4W1w== - dependencies: - "@sentry/core" "10.12.0" + "@sentry/cli-darwin" "3.4.1" + "@sentry/cli-linux-arm" "3.4.1" + "@sentry/cli-linux-arm64" "3.4.1" + "@sentry/cli-linux-i686" "3.4.1" + "@sentry/cli-linux-x64" "3.4.1" + "@sentry/cli-win32-arm64" "3.4.1" + "@sentry/cli-win32-i686" "3.4.1" + "@sentry/cli-win32-x64" "3.4.1" + +"@sentry/core@10.51.0": + version "10.51.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-10.51.0.tgz#bf103c6f03882311b93258f5424b777b24d4fb4f" + integrity sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w== + +"@sentry/expo-upload-sourcemaps@8.10.0": + version "8.10.0" + resolved "https://registry.yarnpkg.com/@sentry/expo-upload-sourcemaps/-/expo-upload-sourcemaps-8.10.0.tgz#d83bfa749e14c356b352232b111b7cf819059a6a" + integrity sha512-9nca3zuzeohl77Hspkox0CcpCQz11gvplgJMktD0fVLrHKBLW9/KTtAOBSez7FfXe2e8FbF7cd5/Cb5EHyJjpw== + dependencies: + "@sentry/cli" "3.4.1" + +"@sentry/react-native@~8.10.0": + version "8.10.0" + resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-8.10.0.tgz#eeaf79b53758aeee5e993276c616372cfe19a206" + integrity sha512-Pfr7h1unqMsE87UMwaUIZ26VjX7SSsitBLpK4gHeIwYmuXn+qfdYUmme6RnoLlL5IPzu8pCLoRNCvdAJy6eTgw== + dependencies: + "@sentry/babel-plugin-component-annotate" "5.2.1" + "@sentry/browser" "10.51.0" + "@sentry/cli" "3.4.1" + "@sentry/core" "10.51.0" + "@sentry/expo-upload-sourcemaps" "8.10.0" + "@sentry/react" "10.51.0" + "@sentry/types" "10.51.0" + +"@sentry/react@10.51.0": + version "10.51.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-10.51.0.tgz#2c65becc65c4fd0a77470fda00af778dfd94977c" + integrity sha512-RRHHqjNvjji6ebIqdlAr453AkST8Vm4cxdu1vWm772IgbzTO7Jx46Cj6Bt2/GjMyH0YLE5euDaAOQhFMmpvAOw== + dependencies: + "@sentry/browser" "10.51.0" + "@sentry/core" "10.51.0" + +"@sentry/types@10.51.0": + version "10.51.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-10.51.0.tgz#1408ccac89fdbffaefeabac9ad9e7b1f0494458b" + integrity sha512-/0nTcXk82RKtGGv0mxmY56o+BE85lBuSWG9chtSEfeypvxHFyWn3D7td9rPmjboDMtytC24cYbUzx55jb2OjQA== + dependencies: + "@sentry/core" "10.51.0" "@shopify/flash-list@2.0.2": version "2.0.2" @@ -6488,10 +6494,10 @@ cosmiconfig@^9.0.0: js-yaml "^4.1.0" parse-json "^5.2.0" -countly-sdk-react-native-bridge@^25.4.0: - version "25.4.0" - resolved "https://registry.yarnpkg.com/countly-sdk-react-native-bridge/-/countly-sdk-react-native-bridge-25.4.0.tgz#dd04086142becf41b4312c8fe361db87b235e04d" - integrity sha512-MIkQtb5UfWW7FhC7pB6luudlfdTk0YA42YCKtnAwH+0gcm4jkMMuqq0HLytqFWki9fcCzfyatz+HGIu5s5mKvA== +countly-sdk-react-native-bridge@^25.4.1: + version "25.4.1" + resolved "https://registry.yarnpkg.com/countly-sdk-react-native-bridge/-/countly-sdk-react-native-bridge-25.4.1.tgz#068485670d6d0920e3993171d1c2f5550d5128c2" + integrity sha512-6rwQ2TIfh+F1zKsTpat5XtW8v/GQb5SV4Q1Ly0SDpyfsvLfFLh72DaEHzdjRnP5qOMWnG38AICPrz9Fm3DzY/w== create-jest@^29.7.0: version "29.7.0" @@ -8798,7 +8804,7 @@ hey-listen@^1.0.8: resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -8866,7 +8872,7 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" -https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: +https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -14779,6 +14785,11 @@ undici@^6.18.2: resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.3.tgz#185752ad92c3d0efe7a7d1f6854a50f83b552d7a" integrity sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw== +undici@^6.22.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.25.0.tgz#8c4efb8c998dc187fc1cfb5dde1ef19a211849fb" + integrity sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" From a473932a71a43939a701093fa2b05cf23abbdd39 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 6 May 2026 12:50:50 -0700 Subject: [PATCH 2/2] RR-T45 PR#117 fixes --- src/app/call/[id]/index.tsx | 2 +- src/lib/poi.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/call/[id]/index.tsx b/src/app/call/[id]/index.tsx index 0b88a47..bf42735 100644 --- a/src/app/call/[id]/index.tsx +++ b/src/app/call/[id]/index.tsx @@ -774,7 +774,7 @@ export default function CallDetail() { {/* Tabs */} - + setIsNotesModalOpen(false)} callId={callId || ''} /> diff --git a/src/lib/poi.ts b/src/lib/poi.ts index b164cbd..46c5465 100644 --- a/src/lib/poi.ts +++ b/src/lib/poi.ts @@ -35,7 +35,7 @@ export const isPoiMarker = (marker: Pick