Skip to content

Commit 27c72e9

Browse files
author
Lalit Sharma
committed
feat: add geocoding search functionality for address input in Timer and Location Settings screens; implement Photography Guide page plan and related utilities
1 parent 84ac800 commit 27c72e9

10 files changed

Lines changed: 608 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.32] — 2026-02-26
9+
10+
### Added
11+
- Added address/place geocoding search on the `Timer` screen to place the observer pin from text input.
12+
- Added address/place geocoding search on `Location Settings` with coordinate autofill for adding favorites.
13+
- Added reverse-geocoded address label formatting utilities and regression tests for address-label construction.
14+
15+
### Changed
16+
- Updated favorite save flows to prefer reverse-geocoded address naming when the user keeps the default/empty favorite name.
17+
- Updated testing notes to reflect that geocoding search is now implemented (first-match behavior depends on platform geocoder/locale).
18+
- Bumped `apps/mobile` version to `1.1.32`.
19+
20+
### Tests
21+
- Verified mobile checks pass: `pnpm -C apps/mobile typecheck`, `pnpm -C apps/mobile lint`, and `pnpm -C apps/mobile test`.
22+
823
## [1.1.31] — 2026-02-26
924

1025
### Added

apps/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eclipse-timer/mobile",
3-
"version": "1.1.31",
3+
"version": "1.1.32",
44
"private": true,
55
"main": "index.js",
66
"scripts": {

apps/mobile/src/screens/LocationSettingsScreen.tsx

Lines changed: 147 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import * as Location from "expo-location";
12
import { useMemo, useState } from "react";
2-
import { Pressable, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
3+
import { Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
34
import { SafeAreaView } from "react-native-safe-area-context";
45

56
import BurgerButton from "../components/BurgerButton";
7+
import { geocodeAddressQuery, resolveAddressLabelForCoordinates } from "../services/geocoding";
68
import type { FavoriteLocation } from "../state/appState";
79
import { useAppTheme } from "../theme/useAppTheme";
810

@@ -17,6 +19,11 @@ function formatCoordLabel(value: number) {
1719
return value.toFixed(4);
1820
}
1921

22+
function getErrorMessage(err: unknown): string {
23+
if (err instanceof Error && err.message) return err.message;
24+
return String(err);
25+
}
26+
2027
export default function LocationSettingsScreen({
2128
onOpenMenu,
2229
favoriteLocations,
@@ -26,22 +33,78 @@ export default function LocationSettingsScreen({
2633
const { colors } = useAppTheme();
2734
const styles = useMemo(() => createStyles(colors), [colors]);
2835
const [name, setName] = useState("");
36+
const [searchQuery, setSearchQuery] = useState("");
2937
const [latitudeText, setLatitudeText] = useState("");
3038
const [longitudeText, setLongitudeText] = useState("");
39+
const [isSearching, setIsSearching] = useState(false);
40+
const [isSavingFavorite, setIsSavingFavorite] = useState(false);
3141
const [errorMessage, setErrorMessage] = useState<string | null>(null);
3242

3343
const sortedFavorites = useMemo(
3444
() => [...favoriteLocations].sort((a, b) => a.name.localeCompare(b.name)),
3545
[favoriteLocations],
3646
);
3747

38-
const addFavorite = () => {
39-
const trimmedName = name.trim();
40-
if (!trimmedName) {
41-
setErrorMessage("Enter a name for the favorite location.");
48+
const ensureAndroidGeocodePermission = async () => {
49+
if (Platform.OS !== "android") return true;
50+
51+
const existing = await Location.getForegroundPermissionsAsync();
52+
if (existing.status === "granted") return true;
53+
if (!existing.canAskAgain) return false;
54+
55+
const requested = await Location.requestForegroundPermissionsAsync();
56+
return requested.status === "granted";
57+
};
58+
59+
const searchAddress = async () => {
60+
const trimmedQuery = searchQuery.trim();
61+
if (!trimmedQuery || isSearching) {
62+
if (!trimmedQuery) setErrorMessage("Enter an address or place to search.");
4263
return;
4364
}
4465

66+
setIsSearching(true);
67+
setErrorMessage(null);
68+
69+
try {
70+
const hasPermission = await ensureAndroidGeocodePermission();
71+
if (!hasPermission) {
72+
setErrorMessage("Location permission is required to search addresses.");
73+
return;
74+
}
75+
76+
const matches = await geocodeAddressQuery(trimmedQuery);
77+
const first = matches.find(
78+
(item) => Number.isFinite(item.latitude) && Number.isFinite(item.longitude),
79+
);
80+
if (!first) {
81+
setErrorMessage(`No search results for "${trimmedQuery}".`);
82+
return;
83+
}
84+
85+
const lat = first.latitude;
86+
const lon = first.longitude;
87+
setLatitudeText(lat.toFixed(6));
88+
setLongitudeText(lon.toFixed(6));
89+
if (!name.trim()) {
90+
const resolved = await resolveAddressLabelForCoordinates(lat, lon);
91+
if (resolved) {
92+
setName(resolved);
93+
} else {
94+
setName(trimmedQuery);
95+
}
96+
}
97+
setErrorMessage(null);
98+
} catch (err: unknown) {
99+
setErrorMessage(`Address search failed: ${getErrorMessage(err)}`);
100+
} finally {
101+
setIsSearching(false);
102+
}
103+
};
104+
105+
const addFavorite = async () => {
106+
if (isSavingFavorite) return;
107+
45108
const lat = Number(latitudeText);
46109
if (!Number.isFinite(lat) || lat < -90 || lat > 90) {
47110
setErrorMessage("Latitude must be a number between -90 and 90.");
@@ -54,17 +117,37 @@ export default function LocationSettingsScreen({
54117
return;
55118
}
56119

57-
onAddFavoriteLocation({
58-
name: trimmedName,
59-
lat,
60-
lon,
61-
});
62-
setName("");
63-
setLatitudeText("");
64-
setLongitudeText("");
120+
setIsSavingFavorite(true);
65121
setErrorMessage(null);
122+
try {
123+
let resolvedName = name.trim();
124+
if (!resolvedName) {
125+
resolvedName = (await resolveAddressLabelForCoordinates(lat, lon)) ?? "";
126+
}
127+
if (!resolvedName) {
128+
setErrorMessage("Enter a name or search for an address before saving.");
129+
return;
130+
}
131+
132+
onAddFavoriteLocation({
133+
name: resolvedName,
134+
lat,
135+
lon,
136+
});
137+
setName("");
138+
setSearchQuery("");
139+
setLatitudeText("");
140+
setLongitudeText("");
141+
setErrorMessage(null);
142+
} finally {
143+
setIsSavingFavorite(false);
144+
}
66145
};
67146

147+
const canSearchAddress = searchQuery.trim().length > 0 && !isSearching;
148+
const canAddFavorite =
149+
latitudeText.trim().length > 0 && longitudeText.trim().length > 0 && !isSavingFavorite;
150+
68151
return (
69152
<SafeAreaView style={styles.safe} edges={["top", "left", "right", "bottom"]}>
70153
<View style={styles.headerRow}>
@@ -78,10 +161,37 @@ export default function LocationSettingsScreen({
78161
<ScrollView contentContainerStyle={styles.content}>
79162
<View style={styles.formCard}>
80163
<Text style={styles.formTitle}>Add Favorite Location</Text>
164+
<TextInput
165+
value={searchQuery}
166+
onChangeText={setSearchQuery}
167+
placeholder="Search address or place"
168+
placeholderTextColor={colors.inputPlaceholder}
169+
style={styles.input}
170+
autoCapitalize="words"
171+
autoCorrect={false}
172+
returnKeyType="search"
173+
onSubmitEditing={() => {
174+
void searchAddress();
175+
}}
176+
/>
177+
<Pressable
178+
style={[styles.searchBtn, !canSearchAddress ? styles.actionBtnDisabled : null]}
179+
onPress={() => {
180+
void searchAddress();
181+
}}
182+
disabled={!canSearchAddress}
183+
accessibilityRole="button"
184+
accessibilityLabel="Find coordinates for address search query"
185+
accessibilityState={{ disabled: !canSearchAddress }}
186+
>
187+
<Text style={styles.searchBtnText}>
188+
{isSearching ? "Searching..." : "Find Address"}
189+
</Text>
190+
</Pressable>
81191
<TextInput
82192
value={name}
83193
onChangeText={setName}
84-
placeholder="Name (e.g. Austin Home)"
194+
placeholder="Name (optional if address resolves)"
85195
placeholderTextColor={colors.inputPlaceholder}
86196
style={styles.input}
87197
autoCapitalize="words"
@@ -110,8 +220,14 @@ export default function LocationSettingsScreen({
110220
/>
111221
</View>
112222
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
113-
<Pressable style={styles.addBtn} onPress={addFavorite}>
114-
<Text style={styles.addBtnText}>Add Favorite</Text>
223+
<Pressable
224+
style={[styles.addBtn, !canAddFavorite ? styles.actionBtnDisabled : null]}
225+
onPress={() => {
226+
void addFavorite();
227+
}}
228+
disabled={!canAddFavorite}
229+
>
230+
<Text style={styles.addBtnText}>{isSavingFavorite ? "Saving..." : "Add Favorite"}</Text>
115231
</Pressable>
116232
</View>
117233

@@ -199,6 +315,18 @@ function createStyles(colors: ReturnType<typeof useAppTheme>["colors"]) {
199315
paddingHorizontal: 12,
200316
fontSize: 14,
201317
},
318+
searchBtn: {
319+
borderRadius: 10,
320+
backgroundColor: colors.primary,
321+
alignItems: "center",
322+
justifyContent: "center",
323+
paddingVertical: 11,
324+
},
325+
searchBtnText: {
326+
color: colors.primaryText,
327+
fontSize: 13,
328+
fontWeight: "700",
329+
},
202330
coordRow: {
203331
flexDirection: "row",
204332
gap: 10,
@@ -217,6 +345,9 @@ function createStyles(colors: ReturnType<typeof useAppTheme>["colors"]) {
217345
justifyContent: "center",
218346
paddingVertical: 11,
219347
},
348+
actionBtnDisabled: {
349+
opacity: 0.7,
350+
},
220351
addBtnText: {
221352
color: colors.primaryText,
222353
fontSize: 14,

0 commit comments

Comments
 (0)