Skip to content

Commit c2bf3c7

Browse files
dfallingclaude
andauthored
Add MapLibre map with user location (#2)
* Add MapLibre map with user location Replace the React Native template view with a MapLibre-powered map using OpenFreeMap's hosted vector tiles. Requests foreground-only location permissions on mount and flies to the user's first fix at zoom 14, with a UserLocation puck + accuracy circle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Mock @maplibre/maplibre-react-native in jest setup The package ships ESM that jest can't parse with the default transform config, causing App.test.tsx to fail. Mock it instead of widening transformIgnorePatterns so tests stay fast and isolated from native code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add pre-push hook running tsc + biome Catches the same TypeScript and lint failures CI surfaces, before the push. Hook lives in `.githooks/` and is wired up via a postinstall script that sets `core.hooksPath` (skipped silently outside a git repo). No new dependency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2bddc32 commit c2bf3c7

7 files changed

Lines changed: 129 additions & 7 deletions

File tree

.githooks/pre-push

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
echo "→ tsc --noEmit"
5+
bunx tsc --noEmit
6+
7+
echo "→ biome check"
8+
bun run lint

App.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66

77
import {ApolloProvider} from '@apollo/client';
8-
import {NewAppScreen} from '@react-native/new-app-screen';
98
import {useEffect} from 'react';
109
import {
1110
ActivityIndicator,
@@ -24,6 +23,7 @@ import {apolloClient, logout} from './src/auth/authClient';
2423
import {useDeepLinkListener} from './src/auth/deepLinks';
2524
import {LoginScreen} from './src/auth/LoginScreen';
2625
import {tokenStore, useAuth, useAuthHydrated} from './src/auth/tokenStore';
26+
import {MapScreen} from './src/map/MapScreen';
2727

2828
function App() {
2929
const isDarkMode = useColorScheme() === 'dark';
@@ -63,10 +63,7 @@ function AppContent() {
6363

6464
return (
6565
<View style={styles.container}>
66-
<NewAppScreen
67-
templateFileName="App.tsx"
68-
safeAreaInsets={safeAreaInsets}
69-
/>
66+
<MapScreen />
7067
<View
7168
style={[styles.logoutBar, {paddingBottom: safeAreaInsets.bottom + 12}]}>
7269
<Text style={styles.signedInAs}>Signed in as {auth.user.email}</Text>

android/app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
22

33
<uses-permission android:name="android.permission.INTERNET" />
4+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
5+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
46

57
<application
68
android:name=".MainApplication"

bun.lock

Lines changed: 31 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jest.setup.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,24 @@ jest.mock('react-native-encrypted-storage', () => ({
77
clear: jest.fn(() => Promise.resolve()),
88
},
99
}));
10+
11+
jest.mock('@maplibre/maplibre-react-native', () => {
12+
const React = require('react');
13+
const passthrough = name => {
14+
const Component = React.forwardRef(({children}, _ref) =>
15+
React.createElement('View', {testID: name}, children),
16+
);
17+
Component.displayName = name;
18+
return Component;
19+
};
20+
return {
21+
__esModule: true,
22+
Map: passthrough('Map'),
23+
Camera: passthrough('Camera'),
24+
UserLocation: passthrough('UserLocation'),
25+
LocationManager: {
26+
requestPermissions: jest.fn(() => Promise.resolve(false)),
27+
},
28+
useCurrentPosition: () => null,
29+
};
30+
});

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
"lint": "biome check",
1010
"lint:fix": "biome check --write",
1111
"format": "biome format --write",
12-
"codegen": "graphql-codegen --config codegen.ts"
12+
"codegen": "graphql-codegen --config codegen.ts",
13+
"postinstall": "git rev-parse --git-dir > /dev/null 2>&1 && git config core.hooksPath .githooks || true"
1314
},
1415
"dependencies": {
1516
"@apollo/client": "^3.11.0",
17+
"@maplibre/maplibre-react-native": "^11.2.1",
1618
"@react-native/new-app-screen": "0.85.3",
1719
"graphql": "^16.9.0",
1820
"react": "19.2.3",

src/map/MapScreen.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {
2+
Camera,
3+
type CameraRef,
4+
LocationManager,
5+
Map as MapLibreMap,
6+
UserLocation,
7+
useCurrentPosition,
8+
} from '@maplibre/maplibre-react-native';
9+
import {useEffect, useRef, useState} from 'react';
10+
import {StyleSheet, View} from 'react-native';
11+
12+
const MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty';
13+
const USER_ZOOM = 14;
14+
15+
export function MapScreen() {
16+
const cameraRef = useRef<CameraRef>(null);
17+
const hasCenteredRef = useRef(false);
18+
const [permissionGranted, setPermissionGranted] = useState(false);
19+
20+
useEffect(() => {
21+
let cancelled = false;
22+
(async () => {
23+
const granted = await LocationManager.requestPermissions();
24+
if (!cancelled && granted) {
25+
setPermissionGranted(true);
26+
}
27+
})();
28+
return () => {
29+
cancelled = true;
30+
};
31+
}, []);
32+
33+
const position = useCurrentPosition({enabled: permissionGranted});
34+
35+
useEffect(() => {
36+
if (hasCenteredRef.current || !position) return;
37+
hasCenteredRef.current = true;
38+
cameraRef.current?.flyTo({
39+
center: [position.coords.longitude, position.coords.latitude],
40+
zoom: USER_ZOOM,
41+
duration: 1500,
42+
});
43+
}, [position]);
44+
45+
return (
46+
<View style={styles.container}>
47+
<MapLibreMap mapStyle={MAP_STYLE} style={styles.map}>
48+
<Camera ref={cameraRef} initialViewState={{center: [0, 20], zoom: 1}} />
49+
<UserLocation animated accuracy />
50+
</MapLibreMap>
51+
</View>
52+
);
53+
}
54+
55+
const styles = StyleSheet.create({
56+
container: {
57+
flex: 1,
58+
},
59+
map: {
60+
flex: 1,
61+
},
62+
});

0 commit comments

Comments
 (0)