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
111 changes: 9 additions & 102 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,21 @@
*/

import {ApolloProvider} from '@apollo/client';
import {useEffect, useState} from 'react';
import {NavigationContainer} from '@react-navigation/native';
import {useEffect} from 'react';
import {
ActivityIndicator,
Pressable,
StatusBar,
StyleSheet,
Text,
useColorScheme,
View,
} from 'react-native';
import {
SafeAreaProvider,
useSafeAreaInsets,
} from 'react-native-safe-area-context';
import {apolloClient, logout} from './src/auth/authClient';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import {apolloClient} from './src/auth/authClient';
import {useDeepLinkListener} from './src/auth/deepLinks';
import {LoginScreen} from './src/auth/LoginScreen';
import {
type AuthUser,
tokenStore,
useAuth,
useAuthHydrated,
} from './src/auth/tokenStore';
import {MapScreen} from './src/map/MapScreen';
import {Sheet} from './src/ui/Sheet';
import {tokenStore, useAuth, useAuthHydrated} from './src/auth/tokenStore';
import {RootNavigator} from './src/navigation/RootNavigator';

function App() {
const isDarkMode = useColorScheme() === 'dark';
Expand Down Expand Up @@ -67,53 +57,9 @@ function AppContent() {
}

return (
<View style={styles.container}>
<MapScreen />
<AccountMenu user={auth.user} />
</View>
);
}

function AccountMenu({user}: {user: AuthUser}) {
const safeAreaInsets = useSafeAreaInsets();
const [open, setOpen] = useState(false);

const initial = user.email.trim().charAt(0).toUpperCase() || '?';

return (
<>
<Pressable
accessibilityLabel="Account menu"
accessibilityRole="button"
onPress={() => setOpen(true)}
style={({pressed}) => [
styles.avatarButton,
{top: safeAreaInsets.top + 12},
pressed && styles.avatarPressed,
]}>
<Text style={styles.avatarText}>{initial}</Text>
</Pressable>
<Sheet
visible={open}
onClose={() => setOpen(false)}
scrimAccessibilityLabel="Close account menu">
<Text style={styles.sheetEmail} numberOfLines={1}>
{user.email}
</Text>
<Pressable
accessibilityRole="button"
onPress={() => {
setOpen(false);
logout();
}}
style={({pressed}) => [
styles.sheetItem,
pressed && styles.sheetItemPressed,
]}>
<Text style={styles.sheetItemText}>Log out</Text>
</Pressable>
</Sheet>
</>
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
);
}

Expand All @@ -125,45 +71,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
avatarButton: {
position: 'absolute',
right: 16,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#1d6fe0',
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOpacity: 0.2,
shadowRadius: 4,
shadowOffset: {width: 0, height: 2},
elevation: 4,
},
avatarPressed: {opacity: 0.8},
avatarText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '600',
},
sheetEmail: {
fontSize: 12,
color: '#666',
paddingHorizontal: 12,
paddingBottom: 8,
},
sheetItem: {
paddingHorizontal: 12,
paddingVertical: 14,
borderRadius: 8,
},
sheetItemPressed: {
backgroundColor: '#f2f2f2',
},
sheetItemText: {
fontSize: 16,
color: '#222',
},
});

export default App;
10 changes: 10 additions & 0 deletions android/app/src/main/java/com/culpeos/app/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.culpeos.app

import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
Expand All @@ -13,6 +14,15 @@ class MainActivity : ReactActivity() {
*/
override fun getMainComponentName(): String = "Culpeos"

/**
* react-native-screens (used by the navigation stack) requires Android to NOT restore the
* fragment hierarchy from a saved instance state, or it crashes recreating native screens.
* Passing null here lets React Native rebuild the view tree itself.
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
}

/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
Expand Down
56 changes: 50 additions & 6 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
module.exports = {
preset: '@react-native/jest-preset',
setupFiles: ['<rootDir>/jest.setup.js'],
// @react-navigation and react-native-screens ship ESM that must be
// transpiled; widen the preset's default allowlist to include them.
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@react-navigation|react-native-screens)/)',
],
};
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@
"@apollo/client": "^3.11.0",
"@maplibre/maplibre-react-native": "^11.2.1",
"@react-native/new-app-screen": "0.85.3",
"@react-navigation/native": "^7.2.5",
"@react-navigation/native-stack": "^7.16.0",
"graphql": "^16.9.0",
"react": "19.2.3",
"react-native": "0.85.3",
"react-native-encrypted-storage": "^4.0.3",
"react-native-safe-area-context": "^5.5.2"
"react-native-safe-area-context": "^5.5.2",
"react-native-screens": "^4.25.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
100 changes: 100 additions & 0 deletions src/account/AccountMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {useState} from 'react';
import {Pressable, StyleSheet, Text} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {logout} from '../auth/authClient';
import {useAuth} from '../auth/tokenStore';
import {Sheet} from '../ui/Sheet';

/**
* Account avatar + sheet, pinned top-right. Lives inside the map screen (rather
* than as a root-level sibling) so pushed detail screens cover it.
*/
export function AccountMenu() {
const safeAreaInsets = useSafeAreaInsets();
const [open, setOpen] = useState(false);
const auth = useAuth();

if (!auth) {
return null;
}
const {user} = auth;
const initial = user.email.trim().charAt(0).toUpperCase() || '?';

return (
<>
<Pressable
accessibilityLabel="Account menu"
accessibilityRole="button"
onPress={() => setOpen(true)}
style={({pressed}) => [
styles.avatarButton,
{top: safeAreaInsets.top + 12},
pressed && styles.avatarPressed,
]}>
<Text style={styles.avatarText}>{initial}</Text>
</Pressable>
<Sheet
visible={open}
onClose={() => setOpen(false)}
scrimAccessibilityLabel="Close account menu">
<Text style={styles.sheetEmail} numberOfLines={1}>
{user.email}
</Text>
<Pressable
accessibilityRole="button"
onPress={() => {
setOpen(false);
logout();
}}
style={({pressed}) => [
styles.sheetItem,
pressed && styles.sheetItemPressed,
]}>
<Text style={styles.sheetItemText}>Log out</Text>
</Pressable>
</Sheet>
</>
);
}

const styles = StyleSheet.create({
avatarButton: {
position: 'absolute',
right: 16,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#1d6fe0',
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOpacity: 0.2,
shadowRadius: 4,
shadowOffset: {width: 0, height: 2},
elevation: 4,
},
avatarPressed: {opacity: 0.8},
avatarText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '600',
},
sheetEmail: {
fontSize: 12,
color: '#666',
paddingHorizontal: 12,
paddingBottom: 8,
},
sheetItem: {
paddingHorizontal: 12,
paddingVertical: 14,
borderRadius: 8,
},
sheetItemPressed: {
backgroundColor: '#f2f2f2',
},
sheetItemText: {
fontSize: 16,
color: '#222',
},
});
26 changes: 13 additions & 13 deletions src/map/ElementDetailModal.tsx → src/map/ElementDetailScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {NativeStackScreenProps} from '@react-navigation/native-stack';
import {
ActivityIndicator,
Image,
Expand All @@ -12,29 +13,24 @@ import {
type ElementDetailQuery,
useElementDetailQuery,
} from '../graphql/__generated__/types';
import {Sheet} from '../ui/Sheet';
import type {RootStackParamList} from '../navigation/types';

type Props = {
elementId: string | null;
onClose: () => void;
};
type Props = NativeStackScreenProps<RootStackParamList, 'ElementDetail'>;

type ElementDetail = ElementDetailQuery['element'];

export function ElementDetailModal({elementId, onClose}: Props) {
const {data, loading} = useElementDetailQuery({
variables: {id: elementId ?? ''},
skip: !elementId,
});
export function ElementDetailScreen({route, navigation}: Props) {
const {elementId} = route.params;
const {data, loading} = useElementDetailQuery({variables: {id: elementId}});

return (
<Sheet visible={elementId !== null} onClose={onClose} variant="fullscreen">
<View style={styles.screen}>
<ModalContents
element={data?.element ?? null}
loading={loading}
onClose={onClose}
onClose={() => navigation.goBack()}
/>
</Sheet>
</View>
);
}

Expand Down Expand Up @@ -167,6 +163,10 @@ function formatSchedule(schedule: NonNullable<ElementDetail['schedule']>) {
}

const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: '#ffffff',
},
container: {
flex: 1,
backgroundColor: '#ffffff',
Expand Down
Loading
Loading