Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@react-navigation/native": "^7.2.5",
"@react-navigation/native-stack": "^7.16.0",
"graphql": "^16.9.0",
"react": "19.2.7",
"react": "19.2.3",
"react-native": "0.85.3",
"react-native-encrypted-storage": "^4.0.3",
"react-native-safe-area-context": "^5.5.2",
Expand Down
17 changes: 13 additions & 4 deletions src/graphql/__generated__/types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 13 additions & 2 deletions src/graphql/queries/elements.graphql
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
query Elements($bounds: GeoBounds, $tripId: String) {
elements(bounds: $bounds, tripId: $tripId) {
query Elements(
$bounds: GeoBounds
$tripId: String
$labels: [String!]
$labelsMatch: LabelMatchMode
) {
elements(
bounds: $bounds
tripId: $tripId
labels: $labels
labelsMatch: $labelsMatch
) {
id
name
icon
labels
location {
id
latitude
Expand Down
1 change: 1 addition & 0 deletions src/graphql/queries/search.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ query Search($query: String!) {
id
name
icon
labels
location {
id
address
Expand Down
23 changes: 21 additions & 2 deletions src/map/ElementDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export function ElementDetailScreen({route, navigation}: Props) {
loading={loading}
onClose={() => navigation.goBack()}
onEdit={() => navigation.navigate('ElementEdit', {elementId})}
// Tapping a label closes the details and adds it to the map's active
// filters. popTo (not navigate) so the detail screen animates backward
// off the stack — closing — rather than pushing a new level forward.
onSelectLabel={label =>
navigation.popTo('Map', {addLabelFilter: label})
}
/>
</View>
);
Expand All @@ -40,11 +46,13 @@ function ModalContents({
loading,
onClose,
onEdit,
onSelectLabel,
}: {
element: ElementDetail | null;
loading: boolean;
onClose: () => void;
onEdit: () => void;
onSelectLabel: (label: string) => void;
}) {
const safeAreaInsets = useSafeAreaInsets();

Expand Down Expand Up @@ -132,9 +140,17 @@ function ModalContents({
<Section title="Labels">
<View style={styles.labelRow}>
{element.labels.map(label => (
<View key={label} style={styles.labelChip}>
<Pressable
key={label}
accessibilityRole="button"
accessibilityLabel={`Filter map by label ${label}`}
onPress={() => onSelectLabel(label)}
style={({pressed}) => [
styles.labelChip,
pressed && styles.labelChipPressed,
]}>
<Text style={styles.labelChipText}>{label}</Text>
</View>
</Pressable>
))}
</View>
</Section>
Expand Down Expand Up @@ -305,6 +321,9 @@ const styles = StyleSheet.create({
paddingVertical: 5,
borderRadius: 12,
},
labelChipPressed: {
backgroundColor: '#dde7f7',
},
labelChipText: {
color: '#1d6fe0',
fontSize: 12,
Expand Down
139 changes: 139 additions & 0 deletions src/map/FilterChips.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {Pressable, StyleSheet, Text, View} from 'react-native';

// Stands in for the trip's emoji on a label pill, so the two filter kinds read
// differently while sharing one pill style.
const LABEL_ICON = '🏷️';

type Props = {
/** Active trip filter, or null when not filtering by trip. */
trip: {icon: string; name: string} | null;
/** Active label filters; rendered as one pill each. */
labels: readonly string[];
topOffset: number;
onClearTrip: () => void;
onClearLabel: (label: string) => void;
};

/**
* Floating row of active map filters. Shows the trip filter (if any) followed by
* a pill per active label; each pill has an `×` to clear just that filter. Trip
* and label filters combine, so several pills can be shown at once and they wrap
* onto further lines when they don't fit.
*/
export function FilterChips({
trip,
labels,
topOffset,
onClearTrip,
onClearLabel,
}: Props) {
if (!trip && labels.length === 0) return null;
return (
<View style={[styles.wrap, {top: topOffset}]} pointerEvents="box-none">
<View style={styles.row}>
{trip ? (
<FilterPill
icon={trip.icon}
title={trip.name}
accessibilityLabel="Clear trip filter"
onClear={onClearTrip}
/>
) : null}
{labels.map(label => (
<FilterPill
key={label}
icon={LABEL_ICON}
title={label}
accessibilityLabel={`Clear ${label} filter`}
onClear={() => onClearLabel(label)}
/>
))}
</View>
</View>
);
}

/** A single filter pill: leading icon, title, and an `×` to clear it. */
function FilterPill({
icon,
title,
accessibilityLabel,
onClear,
}: {
icon: string;
title: string;
accessibilityLabel: string;
onClear: () => void;
}) {
return (
<View style={styles.pill}>
{icon ? <Text style={styles.icon}>{icon}</Text> : null}
<Text style={styles.title} numberOfLines={1}>
{title}
</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel={accessibilityLabel}
hitSlop={10}
onPress={onClear}
style={styles.clearButton}>
<Text style={styles.clearIcon}>×</Text>
</Pressable>
</View>
);
}

const styles = StyleSheet.create({
wrap: {
position: 'absolute',
left: 16,
right: 16,
alignItems: 'center',
},
row: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
gap: 8,
},
pill: {
flexDirection: 'row',
alignItems: 'center',
maxWidth: '100%',
backgroundColor: '#ffffff',
borderRadius: 20,
paddingLeft: 14,
paddingRight: 6,
paddingVertical: 6,
shadowColor: '#000',
shadowOpacity: 0.2,
shadowRadius: 4,
shadowOffset: {width: 0, height: 2},
elevation: 4,
},
icon: {
fontSize: 15,
lineHeight: 18,
marginRight: 6,
},
title: {
flexShrink: 1,
fontSize: 14,
fontWeight: '600',
color: '#1d6fe0',
},
clearButton: {
width: 24,
height: 24,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 8,
backgroundColor: '#eef3fb',
},
clearIcon: {
color: '#1d6fe0',
fontSize: 16,
lineHeight: 18,
},
});
Loading
Loading