Skip to content

Commit

Permalink
feat: Adds ability to search for favorites. fixes #422 (#590)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikaelbr authored Oct 23, 2020
1 parent cf375ab commit 5b04f11
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 44 deletions.
3 changes: 1 addition & 2 deletions src/favorites/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {LocationFavorite} from './types';
import React from 'react';
import {Text} from 'react-native';
import {MapPointPin} from '../assets/svg/icons/places';

export type FavoriteIconProps = {
favorite?: LocationFavorite;
favorite?: {emoji?: string};
};

export function FavoriteIcon({favorite}: FavoriteIconProps) {
Expand Down
78 changes: 58 additions & 20 deletions src/location-search/LocationResults.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React from 'react';
import {View, Text, TouchableOpacity} from 'react-native';
import {StyleSheet} from '../theme';
import {Location} from '../favorites/types';
import LocationIcon from '../components/location-icon';
import insets from '../utils/insets';
import {ArrowUpLeft} from '../assets/svg/icons/navigation';
import {LocationSearchResult} from './types';
import {FavoriteIcon} from '../favorites';

type Props = {
title: string;
locations: Location[];
onSelect: (location: Location) => void;
title?: string;
locations: LocationSearchResult[];
onSelect: (location: LocationSearchResult) => void;
onPrefillText: (text: string) => void;
};

Expand All @@ -22,40 +23,48 @@ const LocationResults: React.FC<Props> = ({
const styles = useThemeStyles();
return (
<>
<View accessibilityRole="header" style={styles.subHeader}>
<Text style={styles.subLabel}>{title}</Text>
<View style={styles.subBar} />
</View>
{title && (
<View accessibilityRole="header" style={styles.subHeader}>
<Text style={styles.subLabel}>{title}</Text>
<View style={styles.subBar} />
</View>
)}
<View style={styles.list}>
{locations.map((location) => (
<View style={styles.rowContainer} key={location.id}>
{locations.map(mapToVisibleSearchResult).map((searchResult) => (
<View style={styles.rowContainer} key={searchResult.key}>
<View style={styles.locationButtonContainer}>
<TouchableOpacity
accessible={true}
accessibilityRole="menuitem"
hitSlop={insets.symmetric(8, 1)}
onPress={() => onSelect(location)}
onPress={() => onSelect(searchResult.selectable)}
style={styles.locationButton}
>
<View style={{flexDirection: 'column'}}>
<LocationIcon
location={location}
fill={String(styles.locationIcon.backgroundColor)}
multiple={true}
/>
{searchResult.emoji ? (
<FavoriteIcon favorite={searchResult} />
) : (
<LocationIcon
location={searchResult.location}
fill={String(styles.locationIcon.backgroundColor)}
multiple={true}
/>
)}
</View>
<View style={styles.locationTextContainer}>
<Text style={styles.locationName}>{location.name}</Text>
<Text style={styles.locality}>{location.locality}</Text>
<Text style={styles.locationName}>{searchResult.text}</Text>
<Text style={styles.locality}>{searchResult.subtext}</Text>
</View>
</TouchableOpacity>
</View>
<TouchableOpacity
accessible={true}
accessibilityLabel={'Legg ' + location.name + ' i søkefelt'}
accessibilityLabel={
'Legg ' + searchResult.prefill + ' i søkefelt'
}
accessibilityRole="button"
hitSlop={insets.all(8)}
onPress={() => onPrefillText(location.name + ' ')}
onPress={() => onPrefillText(searchResult.prefill + ' ')}
>
<ArrowUpLeft />
</TouchableOpacity>
Expand All @@ -66,6 +75,35 @@ const LocationResults: React.FC<Props> = ({
);
};

function mapToVisibleSearchResult(searchResult: LocationSearchResult) {
const location = searchResult.location;
if (!searchResult.favoriteInfo) {
return {
key: location.id,
selectable: searchResult,
location,
text: location.name,
subtext: location.locality,
prefill: location.name,
};
}

const text = searchResult.favoriteInfo.name ?? location.name;
const subtext = searchResult.favoriteInfo.name
? `${location.name}, ${location.locality}`
: location.locality;

return {
key: searchResult.favoriteInfo.id,
selectable: searchResult,
location,
text,
subtext,
prefill: location.name,
emoji: searchResult.favoriteInfo.emoji,
};
}

export default LocationResults;

const useThemeStyles = StyleSheet.createThemeHook((theme) => ({
Expand Down
80 changes: 58 additions & 22 deletions src/location-search/LocationSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import {Text, TextInput, View, Keyboard} from 'react-native';
import {ScrollView} from 'react-native-gesture-handler';
import Header from '../ScreenHeader';
import Input from '../components/input';
import {Location, LocationWithMetadata} from '../favorites/types';
import {
Location,
LocationWithMetadata,
UserFavorites,
} from '../favorites/types';
import {useGeolocationState} from '../GeolocationContext';
import {RootStackParamList} from '../navigation';
import {useSearchHistory} from '../search-history';
Expand All @@ -20,6 +24,8 @@ import {SafeAreaView} from 'react-native-safe-area-context';
import {useGeocoder} from '../geocoder';
import MessageBox from '../message-box';
import {ErrorType} from '../api/utils';
import {useFavorites} from '../favorites';
import {LocationSearchResult} from './types';

export type Props = {
navigation: LocationSearchNavigationProp;
Expand Down Expand Up @@ -48,16 +54,18 @@ const LocationSearch: React.FC<Props> = ({
}) => {
const styles = useThemeStyles();
const {history, addSearchEntry} = useSearchHistory();
const {favorites} = useFavorites();

const [text, setText] = useState<string>(initialLocation?.name ?? '');
const debouncedText = useDebounce(text, 200);

const previousLocations = filterPreviousLocations(debouncedText, history);
const previousLocations = filterPreviousLocations(
debouncedText,
history,
favorites,
);

const {
location: geolocation,
requestPermission: requestGeoPermission,
} = useGeolocationState();
const {location: geolocation} = useGeolocationState();

const {locations, error} =
useGeocoder(debouncedText, geolocation?.coords ?? null) ?? [];
Expand Down Expand Up @@ -90,8 +98,16 @@ const LocationSearch: React.FC<Props> = ({
});
};

const onSearchSelect = (location: Location) =>
onSelect({...location, resultType: 'search'});
const onSearchSelect = (searchResult: LocationSearchResult) => {
if (!searchResult.favoriteInfo) {
return onSelect({...searchResult.location, resultType: 'search'});
}
return onSelect({
...searchResult.location,
resultType: 'favorite',
favoriteId: searchResult.favoriteInfo.id,
});
};

const inputRef = useRef<TextInput>(null);

Expand Down Expand Up @@ -172,7 +188,6 @@ const LocationSearch: React.FC<Props> = ({
>
{hasPreviousResults && (
<LocationResults
title="Tidligere søk"
locations={previousLocations}
onSelect={onSearchSelect}
onPrefillText={onPrefillText}
Expand Down Expand Up @@ -215,22 +230,43 @@ function translateErrorType(errorType: ErrorType): string {
const filterPreviousLocations = (
searchText: string,
previousLocations: Location[],
): Location[] =>
searchText
? previousLocations.filter((l) =>
l.name?.toLowerCase()?.startsWith(searchText.toLowerCase()),
)
: previousLocations;
favorites?: UserFavorites,
): LocationSearchResult[] => {
const mappedHistory: LocationSearchResult[] =
previousLocations?.map((location) => ({
location,
})) ?? [];

if (!searchText) {
return mappedHistory;
}

const matchText = (text?: string) =>
text?.toLowerCase()?.startsWith(searchText.toLowerCase());
const filteredFavorites: LocationSearchResult[] = (favorites ?? [])
.filter(
(favorite) =>
matchText(favorite.location?.name) || matchText(favorite.name),
)
.map(({location, ...favoriteInfo}) => ({
location,
favoriteInfo,
}));

return filteredFavorites.concat(
mappedHistory.filter((l) => matchText(l.location.name)),
);
};

const filterCurrentLocation = (
locations: Location[] | null,
previousLocations: Location[] | null,
): Location[] => {
if (!previousLocations?.length) return locations ?? [];
if (!locations) return [];
return locations.filter(
(l) => !previousLocations.some((pl) => pl.id === l.id),
);
previousLocations: LocationSearchResult[] | null,
): LocationSearchResult[] => {
if (!previousLocations?.length || !locations)
return locations?.map((location) => ({location})) ?? [];
return locations
.filter((l) => !previousLocations.some((pl) => pl.location.id === l.id))
.map((location) => ({location}));
};

const useThemeStyles = StyleSheet.createThemeHook((theme) => ({
Expand Down
6 changes: 6 additions & 0 deletions src/location-search/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {Location, LocationFavorite} from '../favorites/types';

export type LocationSearchResult = {
location: Location;
favoriteInfo?: Omit<LocationFavorite, 'location'>;
};

0 comments on commit 5b04f11

Please sign in to comment.