From 648acb95cbea6253330a2b5d1b752908629929e1 Mon Sep 17 00:00:00 2001 From: Hank Date: Mon, 6 Jul 2026 11:05:42 -0700 Subject: [PATCH] Meter screen: theme it + fix rate/max-time/policy display; add camera trace logs - MeterDetail now themed (fixes unreadable black headings in dark mode) - Rate no longer double-'$'; MaxTime 0 -> "Unlimited"; policies show a clean title + time window and hide sentinel (<=0) rates - Temporary [CAM] logging on the map to trace the persistent recenter-on-search Co-Authored-By: Claude Fable 5 --- app/src/screens/MapScreen.tsx | 22 ++++- app/src/screens/MeterDetailScreen.tsx | 122 ++++++++++++++++---------- 2 files changed, 95 insertions(+), 49 deletions(-) diff --git a/app/src/screens/MapScreen.tsx b/app/src/screens/MapScreen.tsx index 4252701..58242d7 100644 --- a/app/src/screens/MapScreen.tsx +++ b/app/src/screens/MapScreen.tsx @@ -76,6 +76,7 @@ export function MapScreen() { useEffect(() => { if (mapReady && initialCenter && !positioned.current) { positioned.current = true; + console.log('[CAM] initial-positioning ->', JSON.stringify(initialCenter)); cameraRef.current?.setCamera?.({ centerCoordinate: [initialCenter.longitude, initialCenter.latitude], zoomLevel: initialCenter === DEFAULT_CENTER ? DEFAULT_ZOOM : 14, @@ -108,13 +109,15 @@ export function MapScreen() { const searchThisArea = async () => { try { const center = await mapRef.current?.getCenter?.(); // [lng, lat] + console.log('[CAM] searchThisArea getCenter=', JSON.stringify(center)); if (center && center.length === 2) { await searchAt({ latitude: center[1], longitude: center[0] }, 'this area'); return; } - } catch { - /* fall through */ + } catch (err) { + console.log('[CAM] getCenter threw', String(err)); } + console.log('[CAM] searchThisArea FALLBACK coords=', JSON.stringify(coords)); if (coords) await searchAt(coords, 'this area'); }; @@ -126,6 +129,7 @@ export function MapScreen() { setStatus('No location fix yet — GPS may be unavailable (e.g. indoors).'); return; } + console.log('[CAM] goToMyLocation setCamera ->', JSON.stringify(c)); cameraRef.current?.setCamera?.({ centerCoordinate: [c.longitude, c.latitude], zoomLevel: 15, @@ -140,6 +144,7 @@ export function MapScreen() { setStatus('No saved location yet — enable location once to cache it.'); return; } + console.log('[CAM] searchLastKnown setCamera ->', JSON.stringify(last)); cameraRef.current?.setCamera?.({ centerCoordinate: [last.longitude, last.latitude], zoomLevel: 15, @@ -180,7 +185,18 @@ export function MapScreen() { style={styles.map} mapStyle={mapStyle} rotateEnabled={false} - onDidFinishLoadingMap={() => setMapReady(true)} + onDidFinishLoadingMap={() => { + console.log('[CAM] onDidFinishLoadingMap (mapReady already', mapReady, ')'); + setMapReady(true); + }} + onRegionDidChange={(f: any) => + console.log( + '[CAM] regionDidChange center=', + JSON.stringify(f?.geometry?.coordinates), + 'userInteraction=', + f?.properties?.isUserInteraction, + ) + } > {/* Uncontrolled camera; we position it imperatively (once on load, then only on explicit user actions) so re-renders never move the map. */} diff --git a/app/src/screens/MeterDetailScreen.tsx b/app/src/screens/MeterDetailScreen.tsx index bba85a3..a7b64ef 100644 --- a/app/src/screens/MeterDetailScreen.tsx +++ b/app/src/screens/MeterDetailScreen.tsx @@ -4,10 +4,38 @@ import type { RouteProp } from '@react-navigation/native'; import { useRoute } from '@react-navigation/native'; import type { RootStackParamList } from '@/navigation/RootNavigator'; import { saveKiosk, toSavedKiosk } from '@/features/favorites/favoritesStore'; +import { useTheme } from '@/theme/ThemeContext'; +import type { SpacePolicy } from 'parksmarter-client'; type DetailRoute = RouteProp; +/** MaxTime of 0 means no cap. */ +function fmtMaxTime(m?: number): string { + if (m == null) return '—'; + if (m <= 0) return 'Unlimited'; + return `${m} min`; +} + +/** Zone rate comes as a string that sometimes already includes a '$'. */ +function fmtRate(r?: string): string | undefined { + if (r == null) return undefined; + return '$' + String(r).replace(/^\$+/, ''); +} + +function policyLine(p: SpacePolicy): { title: string; sub: string } { + const title = p.DisplayString || p.MessageHeader || p.RateType || 'Rate policy'; + const parts: string[] = []; + if (p.StartTimeDisplay && p.EndTimeDisplay) { + parts.push(`${p.StartTimeDisplay}–${p.EndTimeDisplay}`); + } + // Negative/zero rates are sentinels ("no charge" / "n/a") — only show real ones. + if (typeof p.Rate === 'number' && p.Rate > 0) parts.push(`$${p.Rate}/hr`); + if (p.MaxTime != null) parts.push(`${fmtMaxTime(p.MaxTime)} max`); + return { title, sub: parts.join(' · ') }; +} + export function MeterDetailScreen() { + const { colors } = useTheme(); const { params } = useRoute(); const z = params.zone; const [saved, setSaved] = useState(false); @@ -20,21 +48,32 @@ export function MeterDetailScreen() { const firstSpace = z.Spaces?.[0]; + const Field = ({ label, value }: { label: string; value?: string }) => ( + + {label} + {value ?? '—'} + + ); + return ( - - {z.ZoneName ?? 'Parking meter'} - {z.ZoneLocation ? {z.ZoneLocation} : null} + + + {z.ZoneName ?? 'Parking meter'} + + {z.ZoneLocation ? ( + {z.ZoneLocation} + ) : null} - - + + {firstSpace?.Policies?.length ? ( - - Rate policies - {firstSpace.Policies.slice(0, 6).map((p, i) => ( - - • {p.DisplayString ?? p.RateType ?? 'Policy'} - {p.Rate != null ? ` — $${p.Rate}` : ''} - - ))} + + Rate policies + {firstSpace.Policies.slice(0, 8).map((p, i) => { + const { title, sub } = policyLine(p); + return ( + + {title} + {sub ? ( + {sub} + ) : null} + + ); + })} ) : null} - {saved ? 'Saved ✓' : 'Save kiosk'} + + {saved ? 'Saved ✓' : 'Save kiosk'} + Alert.alert( 'Start session', @@ -73,40 +119,24 @@ export function MeterDetailScreen() { ) } > - Start parking session + Start parking session ); } -function Field({ label, value }: { label: string; value?: string }) { - return ( - - {label} - {value ?? '—'} - - ); -} - const styles = StyleSheet.create({ - container: { flex: 1 }, title: { fontSize: 24, fontWeight: '700' }, - sub: { color: '#666', marginBottom: 12 }, + sub: { marginBottom: 12 }, row: { flexDirection: 'row', gap: 12, marginTop: 8 }, - field: { flex: 1, backgroundColor: '#f2f2f2', borderRadius: 10, padding: 12 }, - fieldLabel: { fontSize: 12, color: '#888' }, + field: { flex: 1, borderRadius: 10, padding: 12 }, + fieldLabel: { fontSize: 12 }, fieldValue: { fontSize: 16, fontWeight: '600', marginTop: 2 }, - card: { backgroundColor: '#f7f7f7', borderRadius: 10, padding: 14, marginTop: 16 }, - cardTitle: { fontWeight: '700', marginBottom: 6 }, - policy: { color: '#444', marginBottom: 2 }, - button: { - marginTop: 16, - backgroundColor: '#555', - borderRadius: 10, - padding: 16, - alignItems: 'center', - }, - buttonPrimary: { backgroundColor: '#1e6f5c' }, - buttonSaved: { backgroundColor: '#2e7d32' }, - buttonText: { color: '#fff', fontWeight: '600', fontSize: 16 }, + card: { borderRadius: 10, padding: 14, marginTop: 16 }, + cardTitle: { fontWeight: '700', marginBottom: 8, fontSize: 15 }, + policyRow: { marginBottom: 8 }, + policyTitle: { fontSize: 14, fontWeight: '600' }, + policySub: { fontSize: 12, marginTop: 1 }, + button: { marginTop: 16, borderRadius: 10, padding: 16, alignItems: 'center' }, + buttonText: { fontWeight: '600', fontSize: 16 }, });