From a4150555ae6c8e480cc5219e21c8a33f7a28f50d Mon Sep 17 00:00:00 2001 From: Hank Date: Mon, 6 Jul 2026 11:14:23 -0700 Subject: [PATCH] Fix map recenter (root cause), login logo/theming, policy pricing display - ROOT CAUSE: MapLibre Camera re-applies its last setCamera() stop on every re-render, so a search (setZones) snapped the map back to the last programmatic target. Memoize the Camera + UserLocation elements so search re-renders never touch them; only the marker ShapeSource updates. (Confirmed via [CAM] trace.) - Login: brain-car logo + full theming so text is readable in light/dark - Meter policies: negative Rate is a sentinel (RateType No Parking=-1 / Free=-3), not a price; show DisplayString + colored type dot, real $/hr only when positive. Fix zone rate "$ 4.00" formatting. Co-Authored-By: Claude Fable 5 --- app/src/screens/LoginScreen.tsx | 51 +++++++++++++++------------ app/src/screens/MapScreen.tsx | 44 +++++++++++++---------- app/src/screens/MeterDetailScreen.tsx | 49 +++++++++++++++---------- 3 files changed, 83 insertions(+), 61 deletions(-) diff --git a/app/src/screens/LoginScreen.tsx b/app/src/screens/LoginScreen.tsx index eef0e85..fa7a2c6 100644 --- a/app/src/screens/LoginScreen.tsx +++ b/app/src/screens/LoginScreen.tsx @@ -1,18 +1,20 @@ import React, { useState } from 'react'; import { ActivityIndicator, + Image, KeyboardAvoidingView, Platform, StyleSheet, Text, TextInput, TouchableOpacity, - View, } from 'react-native'; import { useAuth } from '@/auth/AuthContext'; +import { useTheme } from '@/theme/ThemeContext'; export function LoginScreen() { const { login, error } = useAuth(); + const { colors } = useTheme(); const [phone, setPhone] = useState(''); const [password, setPassword] = useState(''); const [busy, setBusy] = useState(false); @@ -30,23 +32,32 @@ export function LoginScreen() { return ( - BigBrainParking - Sign in with your phone number + + BigBrainParking + + Sign in with your phone number + {error} : null} @@ -66,10 +77,9 @@ export function LoginScreen() { )} - - - Forgot your password? Use the official app or a reset SMS — this build reuses - the same account. + + Forgot your password? Reset it via the official app — this build reuses the same + account. ); @@ -77,24 +87,19 @@ export function LoginScreen() { const styles = StyleSheet.create({ container: { flex: 1, padding: 24, justifyContent: 'center' }, - title: { fontSize: 32, fontWeight: '700', textAlign: 'center' }, - subtitle: { fontSize: 15, color: '#666', textAlign: 'center', marginBottom: 24 }, + logo: { width: 112, height: 112, alignSelf: 'center', marginBottom: 12, borderRadius: 24 }, + title: { fontSize: 32, fontWeight: '800', textAlign: 'center' }, + subtitle: { fontSize: 15, textAlign: 'center', marginBottom: 24 }, input: { borderWidth: 1, - borderColor: '#ccc', borderRadius: 10, padding: 14, fontSize: 16, marginBottom: 12, }, - error: { color: '#c0392b', marginBottom: 12 }, - button: { - backgroundColor: '#1e6f5c', - borderRadius: 10, - padding: 16, - alignItems: 'center', - }, + error: { color: '#e0574a', marginBottom: 12, fontWeight: '600', textAlign: 'center' }, + button: { borderRadius: 10, padding: 16, alignItems: 'center' }, buttonDisabled: { opacity: 0.6 }, - buttonText: { color: '#fff', fontWeight: '600', fontSize: 16 }, - hint: { color: '#888', fontSize: 12, textAlign: 'center' }, + buttonText: { color: '#fff', fontWeight: '700', fontSize: 16 }, + hint: { fontSize: 12, textAlign: 'center', marginTop: 16 }, }); diff --git a/app/src/screens/MapScreen.tsx b/app/src/screens/MapScreen.tsx index 58242d7..2857600 100644 --- a/app/src/screens/MapScreen.tsx +++ b/app/src/screens/MapScreen.tsx @@ -178,6 +178,29 @@ export function MapScreen() { } }; + // MapLibre's Camera re-applies its last imperative setCamera() stop whenever it + // re-renders — so a search re-render (setZones) would snap the map back to the + // last programmatic target. Memoize these elements so search re-renders never + // touch them; only the marker ShapeSource below updates. + const cameraEl = useMemo(() => , []); + const userLocationEl = useMemo( + () => ( + { + if (loc?.coords) { + nativeFix.current = { + latitude: loc.coords.latitude, + longitude: loc.coords.longitude, + }; + } + }} + /> + ), + [], + ); + return ( - {/* Uncontrolled camera; we position it imperatively (once on load, then - only on explicit user actions) so re-renders never move the map. */} - - - {/* renderMode="normal" draws the dot in the JS layer and does NOT engage - the native location component, which was tracking the camera back to - the user on re-render. */} - { - if (loc?.coords) { - nativeFix.current = { - latitude: loc.coords.latitude, - longitude: loc.coords.longitude, - }; - } - }} - /> + {cameraEl} + {userLocationEl} 0) parts.push(`$${p.Rate}/hr`); - if (p.MaxTime != null) parts.push(`${fmtMaxTime(p.MaxTime)} max`); - return { title, sub: parts.join(' · ') }; + const rate = typeof p.Rate === 'number' && p.Rate > 0 ? `$${p.Rate}/hr` : undefined; + return { title, rate, color }; } export function MeterDetailScreen() { @@ -86,13 +91,18 @@ export function MeterDetailScreen() { {firstSpace?.Policies?.length ? ( Rate policies - {firstSpace.Policies.slice(0, 8).map((p, i) => { - const { title, sub } = policyLine(p); + {firstSpace.Policies.slice(0, 10).map((p, i) => { + const { title, rate, color } = policyMeta(p); return ( - {title} - {sub ? ( - {sub} + + + {title} + + {rate ? ( + {rate} ) : null} ); @@ -134,9 +144,10 @@ const styles = StyleSheet.create({ fieldValue: { fontSize: 16, fontWeight: '600', marginTop: 2 }, 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 }, + policyRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 10 }, + policyDot: { width: 9, height: 9, borderRadius: 5 }, + policyTitle: { fontSize: 14, fontWeight: '600', flex: 1 }, + policyRate: { fontSize: 13, fontWeight: '600' }, button: { marginTop: 16, borderRadius: 10, padding: 16, alignItems: 'center' }, buttonText: { fontWeight: '600', fontSize: 16 }, });