Use @gorhom/bottom-sheet for waypoint sheet
Replace custom View-based bottom sheet with @gorhom/bottom-sheet: - Native gesture-driven drag, snap points, pan-down-to-close - Proper safe area handling built in - Wrap app with GestureHandlerRootView - Add react-native-reanimated plugin to Expo config Adds react-native-reanimated and react-native-gesture-handler as native dependencies (requires new EAS build). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe07fbdf59
commit
bdde95e69b
6 changed files with 782 additions and 250 deletions
|
|
@ -44,6 +44,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
||||||
project: "mobile",
|
project: "mobile",
|
||||||
}],
|
}],
|
||||||
"expo-sqlite",
|
"expo-sqlite",
|
||||||
|
"react-native-reanimated",
|
||||||
],
|
],
|
||||||
extra: {
|
extra: {
|
||||||
eas: {
|
eas: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Stack, router } from "expo-router";
|
import { Stack, router } from "expo-router";
|
||||||
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
import { Sentry, initSentry } from "../lib/sentry";
|
import { Sentry, initSentry } from "../lib/sentry";
|
||||||
import { isAuthenticated } from "../lib/auth";
|
import { isAuthenticated } from "../lib/auth";
|
||||||
import { startVersionCheck } from "../lib/version-check";
|
import { startVersionCheck } from "../lib/version-check";
|
||||||
|
|
@ -22,7 +23,11 @@ function RootLayout() {
|
||||||
|
|
||||||
if (!checked) return null;
|
if (!checked) return null;
|
||||||
|
|
||||||
return <Stack screenOptions={{ headerShown: false }} />;
|
return (
|
||||||
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
|
<Stack screenOptions={{ headerShown: false }} />
|
||||||
|
</GestureHandlerRootView>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Sentry.wrap(RootLayout);
|
export default Sentry.wrap(RootLayout);
|
||||||
|
|
|
||||||
|
|
@ -172,15 +172,13 @@ function RouteDetailContent({ route }: { route: RouteDetail }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedWaypoint !== null && editor.waypoints[selectedWaypoint] && (
|
{selectedWaypoint !== null && editor.waypoints[selectedWaypoint] && (
|
||||||
<View style={[styles.sheetOverlay, { paddingBottom: insets.bottom }]}>
|
<WaypointSheet
|
||||||
<WaypointSheet
|
waypoint={editor.waypoints[selectedWaypoint]!}
|
||||||
waypoint={editor.waypoints[selectedWaypoint]!}
|
index={selectedWaypoint}
|
||||||
index={selectedWaypoint}
|
onClose={() => setSelectedWaypoint(null)}
|
||||||
onClose={() => setSelectedWaypoint(null)}
|
onDelete={editor.deleteWaypoint}
|
||||||
onDelete={editor.deleteWaypoint}
|
onToggleOvernight={editor.toggleOvernight}
|
||||||
onToggleOvernight={editor.toggleOvernight}
|
/>
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -253,5 +251,4 @@ const styles = StyleSheet.create({
|
||||||
actionSecondaryText: { color: "#333", fontSize: 16, fontWeight: "600" },
|
actionSecondaryText: { color: "#333", fontSize: 16, fontWeight: "600" },
|
||||||
errorBanner: { position: "absolute", bottom: 16, alignSelf: "center", backgroundColor: "rgba(220,38,38,0.9)", borderRadius: 8, paddingVertical: 8, paddingHorizontal: 16 },
|
errorBanner: { position: "absolute", bottom: 16, alignSelf: "center", backgroundColor: "rgba(220,38,38,0.9)", borderRadius: 8, paddingVertical: 8, paddingHorizontal: 16 },
|
||||||
errorBannerText: { color: "#fff", fontSize: 13 },
|
errorBannerText: { color: "#fff", fontSize: 13 },
|
||||||
sheetOverlay: { position: "absolute", bottom: 0, left: 0, right: 0 },
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { View, Text, TouchableOpacity, StyleSheet, Alert } from "react-native";
|
import { useRef, useCallback, useMemo } from "react";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { Text, TouchableOpacity, StyleSheet, Alert } from "react-native";
|
||||||
|
import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet";
|
||||||
import type { Waypoint } from "@trails-cool/types";
|
import type { Waypoint } from "@trails-cool/types";
|
||||||
|
|
||||||
interface WaypointSheetProps {
|
interface WaypointSheetProps {
|
||||||
|
|
@ -17,9 +18,10 @@ export function WaypointSheet({
|
||||||
onDelete,
|
onDelete,
|
||||||
onToggleOvernight,
|
onToggleOvernight,
|
||||||
}: WaypointSheetProps) {
|
}: WaypointSheetProps) {
|
||||||
const insets = useSafeAreaInsets();
|
const bottomSheetRef = useRef<BottomSheet>(null);
|
||||||
|
const snapPoints = useMemo(() => ["35%"], []);
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = useCallback(() => {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Delete Waypoint",
|
"Delete Waypoint",
|
||||||
`Remove waypoint ${index + 1}${waypoint.name ? ` (${waypoint.name})` : ""}?`,
|
`Remove waypoint ${index + 1}${waypoint.name ? ` (${waypoint.name})` : ""}?`,
|
||||||
|
|
@ -35,63 +37,55 @@ export function WaypointSheet({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
};
|
}, [index, waypoint.name, onDelete, onClose]);
|
||||||
|
|
||||||
|
const handleSheetChange = useCallback((sheetIndex: number) => {
|
||||||
|
if (sheetIndex === -1) onClose();
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { paddingBottom: 16 + insets.bottom }]}>
|
<BottomSheet
|
||||||
<View style={styles.handle} />
|
ref={bottomSheetRef}
|
||||||
<Text style={styles.title}>
|
snapPoints={snapPoints}
|
||||||
Waypoint {index + 1}{waypoint.name ? `: ${waypoint.name}` : ""}
|
enablePanDownToClose
|
||||||
</Text>
|
onChange={handleSheetChange}
|
||||||
<Text style={styles.coords}>
|
backgroundStyle={styles.background}
|
||||||
{waypoint.lat.toFixed(5)}, {waypoint.lon.toFixed(5)}
|
handleIndicatorStyle={styles.handle}
|
||||||
</Text>
|
>
|
||||||
|
<BottomSheetView style={styles.content}>
|
||||||
<TouchableOpacity
|
<Text style={styles.title}>
|
||||||
style={styles.option}
|
Waypoint {index + 1}{waypoint.name ? `: ${waypoint.name}` : ""}
|
||||||
onPress={() => {
|
</Text>
|
||||||
onToggleOvernight(index);
|
<Text style={styles.coords}>
|
||||||
onClose();
|
{waypoint.lat.toFixed(5)}, {waypoint.lon.toFixed(5)}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={styles.optionIcon}>{waypoint.isDayBreak ? "☀️" : "🌙"}</Text>
|
|
||||||
<Text style={styles.optionText}>
|
|
||||||
{waypoint.isDayBreak ? "Remove overnight stop" : "Mark as overnight stop"}
|
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity style={styles.option} onPress={handleDelete}>
|
<TouchableOpacity
|
||||||
<Text style={styles.optionIcon}>🗑️</Text>
|
style={styles.option}
|
||||||
<Text style={[styles.optionText, styles.deleteText]}>Delete waypoint</Text>
|
onPress={() => {
|
||||||
</TouchableOpacity>
|
onToggleOvernight(index);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.optionIcon}>{waypoint.isDayBreak ? "☀️" : "🌙"}</Text>
|
||||||
|
<Text style={styles.optionText}>
|
||||||
|
{waypoint.isDayBreak ? "Remove overnight stop" : "Mark as overnight stop"}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
<TouchableOpacity style={styles.option} onPress={handleDelete}>
|
||||||
<Text style={styles.closeText}>Close</Text>
|
<Text style={styles.optionIcon}>🗑️</Text>
|
||||||
</TouchableOpacity>
|
<Text style={[styles.optionText, styles.deleteText]}>Delete waypoint</Text>
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
|
</BottomSheetView>
|
||||||
|
</BottomSheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
background: { borderTopLeftRadius: 16, borderTopRightRadius: 16 },
|
||||||
backgroundColor: "#fff",
|
handle: { backgroundColor: "#d1d5db", width: 36 },
|
||||||
borderTopLeftRadius: 16,
|
content: { padding: 16 },
|
||||||
borderTopRightRadius: 16,
|
|
||||||
padding: 16,
|
|
||||||
shadowColor: "#000",
|
|
||||||
shadowOffset: { width: 0, height: -2 },
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 5,
|
|
||||||
},
|
|
||||||
handle: {
|
|
||||||
width: 36,
|
|
||||||
height: 4,
|
|
||||||
borderRadius: 2,
|
|
||||||
backgroundColor: "#d1d5db",
|
|
||||||
alignSelf: "center",
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
title: { fontSize: 16, fontWeight: "600", color: "#111" },
|
title: { fontSize: 16, fontWeight: "600", color: "#111" },
|
||||||
coords: { fontSize: 12, color: "#999", marginTop: 2, marginBottom: 16 },
|
coords: { fontSize: 12, color: "#999", marginTop: 2, marginBottom: 16 },
|
||||||
option: {
|
option: {
|
||||||
|
|
@ -104,12 +98,4 @@ const styles = StyleSheet.create({
|
||||||
optionIcon: { fontSize: 18, marginRight: 12 },
|
optionIcon: { fontSize: 18, marginRight: 12 },
|
||||||
optionText: { fontSize: 15, color: "#333" },
|
optionText: { fontSize: 15, color: "#333" },
|
||||||
deleteText: { color: "#dc2626" },
|
deleteText: { color: "#dc2626" },
|
||||||
closeButton: {
|
|
||||||
marginTop: 12,
|
|
||||||
alignItems: "center",
|
|
||||||
paddingVertical: 12,
|
|
||||||
backgroundColor: "#f3f4f6",
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
closeText: { fontSize: 15, color: "#666" },
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gorhom/bottom-sheet": "^5.2.9",
|
||||||
"@maplibre/maplibre-react-native": "^10.4.2",
|
"@maplibre/maplibre-react-native": "^10.4.2",
|
||||||
"@sentry/cli": "^3.3.5",
|
"@sentry/cli": "^3.3.5",
|
||||||
"@sentry/react-native": "~7.11.0",
|
"@sentry/react-native": "~7.11.0",
|
||||||
|
|
@ -50,6 +51,8 @@
|
||||||
"expo-web-browser": "~55.0.14",
|
"expo-web-browser": "~55.0.14",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-native": "0.83.4",
|
"react-native": "0.83.4",
|
||||||
|
"react-native-gesture-handler": "^2.31.1",
|
||||||
|
"react-native-reanimated": "^4.3.0",
|
||||||
"react-native-safe-area-context": "~5.6.2",
|
"react-native-safe-area-context": "~5.6.2",
|
||||||
"react-native-screens": "~4.23.0",
|
"react-native-screens": "~4.23.0",
|
||||||
"use-latest-callback": "^0.3.3",
|
"use-latest-callback": "^0.3.3",
|
||||||
|
|
@ -57,10 +60,10 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/react-native": "^13.3.3",
|
"@testing-library/react-native": "^13.3.3",
|
||||||
"react-test-renderer": "^19.2.5",
|
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/react": "~19.2.14",
|
"@types/react": "~19.2.14",
|
||||||
"jest-expo": "^55.0.15",
|
"jest-expo": "^55.0.15",
|
||||||
|
"react-test-renderer": "^19.2.5",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
896
pnpm-lock.yaml
generated
896
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue