Edit Profile

parent 87285eae
......@@ -5373,6 +5373,11 @@
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
......@@ -6359,9 +6364,9 @@
}
},
"opencollective-postinstall": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz",
"integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw=="
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q=="
},
"optionator": {
"version": "0.8.3",
......@@ -7189,18 +7194,19 @@
}
},
"react-native-elements": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-elements/-/react-native-elements-2.0.0.tgz",
"integrity": "sha512-xViTU/JlabYX94fDL2iu17gvMtgEOq2lFAToYlU3RBkwb/J13cdwSr8Ti9z6v6Iui4f8S3FjkpRJnFaOsZrK7w==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-native-elements/-/react-native-elements-2.2.1.tgz",
"integrity": "sha512-JhveP4ZQzZrMef9sBJfjfgfdbKafGnqZGosVr4hb3At8c1wJ3QFgOGUsKxhpjpF2wr/RgLbP2UV3rS1cD+t7IA==",
"requires": {
"@types/react-native-vector-icons": "^6.4.4",
"@types/react-native-vector-icons": "^6.4.5",
"color": "^3.1.0",
"deepmerge": "^3.1.0",
"hoist-non-react-statics": "^3.1.0",
"lodash.isequal": "^4.5.0",
"opencollective-postinstall": "^2.0.0",
"prop-types": "^15.7.2",
"react-native-ratings": "^6.5.0",
"react-native-status-bar-height": "^2.2.0"
"react-native-ratings": "^7.2.0",
"react-native-status-bar-height": "^2.5.0"
}
},
"react-native-gesture-handler": {
......@@ -7242,12 +7248,12 @@
}
},
"react-native-ratings": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/react-native-ratings/-/react-native-ratings-6.5.0.tgz",
"integrity": "sha512-YMcfQ7UQCmXGEc/WPlukHSHs5yvckTwjq5fTRk1FG8gaO7fZCNygEUGPuw4Dbvvp3IlsCUn0bOQd63RYsb7NDQ==",
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/react-native-ratings/-/react-native-ratings-7.3.0.tgz",
"integrity": "sha512-NCDIkmrVPnxPzP9zKdlcNpa2rPs3Hiv2qXsojUr3FpwbANWfgYE+jjGSSCBcS3vpXndTjhoaTGFDnybnUSFPFA==",
"requires": {
"lodash": "^4.17.4",
"prop-types": "^15.5.10"
"lodash": "^4.17.15",
"prop-types": "^15.7.2"
}
},
"react-native-reanimated": {
......
import React from 'react';
import {
TouchableWithoutFeedback, Keyboard, View, ViewStyle,
} from 'react-native';
interface Props {
style?: ViewStyle;
}
/**
* Utility component that dismisses keyboard when tapped
*/
const DismissKeyboardView: React.FC<Props> = ({ children, style }) => (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={style}>
{children}
</View>
</TouchableWithoutFeedback>
);
export default DismissKeyboardView;
import React from 'react';
import { StyleSheet, ViewStyle } from 'react-native';
import { StyleSheet, ViewStyle, TextStyle } from 'react-native';
import { Button, Icon } from 'react-native-elements';
import theme from '../theme';
const styles = StyleSheet.create({
container: {
width: '100%',
button: {
borderRadius: theme.spacing.large,
paddingHorizontal: theme.spacing.medium,
},
fullWidth: {
alignSelf: 'stretch',
},
button: {},
});
interface Props {
disabled?: boolean;
fullWidth?: boolean;
icon?: string;
onPress: () => void;
style?: ViewStyle;
title: string;
titleStyle?: TextStyle;
type?: 'solid' | 'clear' | 'outline';
}
const GeoButton: React.FC<Props> = ({
disabled = false,
fullWidth = true,
icon,
style,
title, onPress,
titleStyle,
type = 'solid',
}) => (
<Button
containerStyle={[styles.container, style]}
buttonStyle={[styles.button]}
containerStyle={[fullWidth ? styles.fullWidth : null, style]}
disabled={disabled}
icon={icon ? (
<Icon
......@@ -40,8 +47,8 @@ const GeoButton: React.FC<Props> = ({
) : undefined}
iconContainerStyle={{ marginRight: theme.spacing.xlarge }}
onPress={onPress}
style={styles.button}
title={title}
titleStyle={titleStyle}
type={type}
/>
);
......
......@@ -4,7 +4,7 @@ import { Avatar } from 'react-native-elements';
import { useSelector } from 'react-redux';
import theme from '../theme';
import GeoText from './GeoText';
import { getUserImageUrl } from '../store/auth/AuthSelectors';
import { getUserAvatarUri } from '../store/user/UserSelectors';
const styles = StyleSheet.create({
container: {
......@@ -28,7 +28,7 @@ const ProfileCard: React.FC<Props> = ({
size = 'small',
style,
}) => {
const userImageUrl = useSelector(getUserImageUrl);
const userImageUrl = useSelector(getUserAvatarUri);
return (
<View style={[style, styles.container]}>
{me ? (
......
......@@ -6,36 +6,44 @@ import TagScreen from '../screens/TagScreen';
import HomeNavigator from './HomeNavigator';
import HeaderLeftButton from './HeaderLeftButton';
import theme from '../theme';
import EditProfileModal from '../screens/EditProfileModal';
const Stack = createStackNavigator();
const AppNavigator = () => (
<Stack.Navigator
mode={'modal'}
screenOptions={{headerShown: false}}
screenOptions={({ navigation }) => ({
headerLeft: () => <HeaderLeftButton onPress={() => navigation.goBack()} type={'close'} />,
headerStyle: {
backgroundColor: theme.colors.green,
},
headerTitleStyle: { fontSize: 24, color: theme.colors.white },
})}
>
<Stack.Screen
name={'Home'}
component={HomeNavigator}
options={{ headerShown: false }}
/>
<Stack.Screen
name={'Create'}
component={CreateCacheNavigator}
options={{ headerShown: false }}
/>
<Stack.Screen
name={'CameraModal'}
component={CameraModal}
options={{ headerShown: false }}
/>
<Stack.Screen
name={'TagScreen'}
component={TagScreen}
options={({ navigation }) => ({
headerShown: true,
headerLeft: () => <HeaderLeftButton type='close' onPress={() => navigation.goBack()} />,
headerStyle: {
backgroundColor: theme.colors.green,
},
headerTitleStyle: { fontSize: 24, color: theme.colors.white },
})}
options={{ title: 'Tags' }}
/>
<Stack.Screen
name={'EditProfile'}
component={EditProfileModal}
options={{ title: 'Edit Profile' }}
/>
</Stack.Navigator>
);
......
......@@ -9,6 +9,7 @@ import CacheDetailScreen, { NavParams } from '../screens/CacheDetailScreen';
import RevisitCacheScreen from '../screens/RevisitCacheScreen';
import DevScreen from '../screens/DevScreen';
import GroupScreen from '../screens/GroupScreen';
import ProfileScreen from '../screens/ProfileScreen';
const Stack = createStackNavigator();
const HomeNavigator = () => (
......@@ -42,13 +43,16 @@ const HomeNavigator = () => (
component={RevisitCacheScreen}
options={({ route }) => ({ title: (route.params as NavParams).title })}
/>
<Stack.Screen
name={'Profile'}
component={ProfileScreen}
options={{ title: 'Profile' }}
/>
<Stack.Screen
name={'Settings'}
component={SettingsScreen}
options={{ title: 'Settings' }}
/>
<Stack.Screen
name={'Dev'}
component={DevScreen}
......
......@@ -8,7 +8,8 @@ import Drawer from 'react-native-drawer';
import { Cache } from '../store/caches/CachesState';
import { getDiscoveredCaches } from '../store/caches/CachesSelectors';
import { saveCache, reportCache } from '../store/caches/CachesActions';
import { getAuthToken, getUserImageUrl } from '../store/auth/AuthSelectors';
import { getAuthToken } from '../store/auth/AuthSelectors';
import { getUserAvatarUri } from '../store/user/UserSelectors';
import GeoMarker from '../components/GeoMarker';
import { sampleCaches } from '../SampleCaches';
import { GeoMapSearch, UserLocationProvider } from '../components/GeoMap';
......@@ -60,7 +61,7 @@ const DiscoverScreen: React.FC = () => {
const discoveredCaches: Cache[] = useSelector(getDiscoveredCaches);
const idToken = useSelector(getAuthToken);
const userImageUrl = useSelector(getUserImageUrl);
const userAvatarUri = useSelector(getUserAvatarUri);
const [currCache, setCurrCache] = useState<Cache | null>();
const [modalOpen, setModalOpen] = useState<boolean>(false);
......@@ -116,7 +117,7 @@ const DiscoverScreen: React.FC = () => {
<Avatar
rounded
size={64}
source={{ uri: userImageUrl }}
source={{ uri: userAvatarUri }}
onPress={onDrawerOpen}
/>
</View>
......
import React, {
useState, useMemo, useCallback, useLayoutEffect,
} from 'react';
import { View, StyleSheet } from 'react-native';
import { Avatar, Input } from 'react-native-elements';
import { useNavigation } from '@react-navigation/native';
import { useSelector, useDispatch } from 'react-redux';
import * as ImagePicker from 'expo-image-picker';
import GeoButton from '../components/GeoButton';
import { getUserName, getUserAvatarUri } from '../store/user/UserSelectors';
import theme from '../theme';
import DismissKeyboardView from '../components/DismissKeyboardView';
import { setUserName, setUserAvatarUri } from '../store/user/UserSlice';
const styles = StyleSheet.create({
screen: {
flex: 1,
marginHorizontal: theme.spacing.screenPadding,
},
avatar: {
marginVertical: theme.spacing.medium,
alignSelf: 'center',
},
});
const EditProfileModal = () => {
const dispatch = useDispatch();
const navigation = useNavigation();
// Initial profile state
const initialUserName = useSelector(getUserName);
const initialAvatarUri = useSelector(getUserAvatarUri);
// Editing profile state
const [userNameText, setUserNameText] = useState<string>(initialUserName!);
const [avatarUri, setAvatarUri] = useState<string>(initialAvatarUri);
const isUsernameTooLong: boolean = useMemo(() => userNameText.length > 20, [userNameText]);
const isUsernameValid: boolean = useMemo(() => {
if (userNameText.length === 0) {
return false;
}
return !isUsernameTooLong;
}, [userNameText, isUsernameTooLong]);
const onPickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
quality: 1,
});
if (!result.cancelled) {
setAvatarUri(result.uri);
}
};
const onSavePress = useCallback(() => {
dispatch(setUserName(userNameText));
dispatch(setUserAvatarUri(avatarUri));
navigation.goBack();
}, [dispatch, navigation, userNameText, avatarUri]);
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<GeoButton
onPress={onSavePress}
title={'Save'}
titleStyle={{ color: theme.colors.white }}
disabled={!isUsernameValid}
type={'clear'}
/>
),
});
}, [navigation, onSavePress, isUsernameValid]);
return (
<DismissKeyboardView style={styles.screen}>
<View>
<Avatar
rounded
source={{ uri: avatarUri }}
containerStyle={styles.avatar}
size={'xlarge'}
showAccessory
onAccessoryPress={onPickImage}
/>
<Input
value={userNameText}
onChangeText={setUserNameText}
label={'Username'}
labelStyle={{ color: theme.colors.black }}
placeholder={'Add your name'}
errorMessage={isUsernameTooLong ? 'Must be 20 characters or fewer' : undefined}
/>
</View>
</DismissKeyboardView>
);
};
export default EditProfileModal;
......@@ -19,8 +19,8 @@ import theme from '../theme';
import {
getUserName,
getUserImageUrl,
} from '../store/auth/AuthSelectors';
getUserAvatarUri,
} from '../store/user/UserSelectors';
import { getGroupMemgers, getGroupCaches, getGroup } from '../store/groups/GroupSelectors';
import { Membership } from '../store/groups/GroupState';
......@@ -83,7 +83,7 @@ const renderCache = (item: {item: Cache}) => {
const GroupScreen = () => {
const { params } = useRoute();
const userName = useSelector(getUserName);
const userImageUrl = useSelector(getUserImageUrl);
const userImageUrl = useSelector(getUserAvatarUri);
const { groupId } = params;
......
......@@ -5,7 +5,7 @@ import {
} from 'react-native-elements';
import { useSelector, useDispatch } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { getUserImageUrl, getUserName } from '../store/auth/AuthSelectors';
import { getUserAvatarUri, getUserName } from '../store/user/UserSelectors';
import { getGroups } from '../store/groups/GroupSelectors';
import theme from '../theme';
import GeoText from '../components/GeoText';
......@@ -41,10 +41,15 @@ interface Props {
const MainDrawer: React.FC<Props> = ({ onClose }) => {
const dispatch = useDispatch();
const { navigate } = useNavigation();
const userImageUrl = useSelector(getUserImageUrl);
const userAvatarUri = useSelector(getUserAvatarUri);
const userName = useSelector(getUserName);
const groups = useSelector(getGroups);
const onProfilePress = useCallback(() => {
onClose();
navigate('Profile');
}, [onClose, navigate]);
const onSettingsPress = useCallback(() => {
onClose();
navigate('Settings');
......@@ -66,14 +71,14 @@ const MainDrawer: React.FC<Props> = ({ onClose }) => {
const items: ActionItem[] = useMemo(() => ([
{
icon: 'user-edit',
title: 'Edit profile',
onPress: onSettingsPress,
icon: 'user-alt',
title: 'Profile',
onPress: onProfilePress,
},
{
icon: 'cog',
title: 'Settings',
onPress: () => alert('Settings'),
onPress: onSettingsPress,
},
{
icon: 'wrench',
......@@ -89,7 +94,7 @@ const MainDrawer: React.FC<Props> = ({ onClose }) => {
<Avatar
rounded
size={'large'}
source={{ uri: userImageUrl }}
source={{ uri: userAvatarUri }}
containerStyle={{ marginBottom: theme.spacing.small }}
/>
<GeoText text={userName!} variant={'formHeader'} />
......
import React from 'react';
import { useSelector } from 'react-redux';
import { View, StyleSheet } from 'react-native';
import { Avatar } from 'react-native-elements';
import { useNavigation } from '@react-navigation/native';
import GeoText from '../components/GeoText';
import theme from '../theme';
import {
getUserName,
getUserAvatarUri,
} from '../store/user/UserSelectors';
import GeoButton from '../components/GeoButton';
const styles = StyleSheet.create({
avatar: {
backgroundColor: theme.colors.mediumgray,
marginTop: theme.spacing.medium,
},
});
const ProfileScreen = () => {
const { navigate } = useNavigation();
const userName = useSelector(getUserName);
const userAvatarUri = useSelector(getUserAvatarUri);
const onEditPress = () => navigate('EditProfile');
return (
<View style={{ flex: 1, justifyContent: 'space-between' }}>
<View style={{ alignItems: 'center' }}>
<Avatar
rounded
source={{ uri: userAvatarUri }}
containerStyle={styles.avatar}
size={'xlarge'}
/>
<GeoText
text={userName!}
variant={'formHeader'}
style={{ marginTop: theme.spacing.small }}
/>
<GeoButton
fullWidth={false}
type={'outline'}
title={'Edit profile'}
onPress={onEditPress}
style={{ marginTop: theme.spacing.medium }}
/>
</View>
</View>
);
};
export default ProfileScreen;
import React from 'react';
import { useSelector } from 'react-redux';
import { View, StyleSheet } from 'react-native';
import { Avatar } from 'react-native-elements';
import { useNavigation } from '@react-navigation/native';
import GeoText from '../components/GeoText';
import theme from '../theme';
import { View, Text } from 'react-native';
import {
getUserName,
getUserImageUrl,
} from '../store/auth/AuthSelectors';
const styles = StyleSheet.create({
avatar: {
backgroundColor: theme.colors.mediumgray,
marginTop: theme.spacing.medium,
},
});
const SettingsScreen = () => {
const userName = useSelector(getUserName);
const userImageUrl = useSelector(getUserImageUrl);
return (
<View style={{ flex: 1, justifyContent: 'space-between' }}>
<View style={{ alignItems: 'center' }}>
<Avatar
rounded
source={{ uri: userImageUrl }}
containerStyle={styles.avatar}
size={'xlarge'}
/>
<GeoText
text={userName!}
variant={'formHeader'}
style={{ marginTop: theme.spacing.small }}
/>
</View>
</View>
);
};
const SettingsScreen = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>settings</Text>
</View>
);
export default SettingsScreen;
......@@ -66,14 +66,12 @@ const TagScreen = () => {
</View>
))}
</View>
<View style={{ marginHorizontal: theme.spacing.screenPadding }}>
<GeoButton
title={'Choose'}
onPress={onSubmit}
disabled={!tag}
style={{ marginVertical: theme.spacing.medium }}
/>
</View>
<GeoButton
title={'Choose'}
onPress={onSubmit}
disabled={!tag}
style={{ marginVertical: theme.spacing.medium, marginHorizontal: theme.spacing.screenPadding }}
/>
</ScreenContainer>
);
};
......
......@@ -19,7 +19,7 @@ import IconButton from '../../components/IconButton';
import { getPendingCacheImage, getPendingCacheTag } from '../../store/pendingNewCache/PendingCacheSelectors';
import Tag, { AddTagButton } from '../../components/Tag';
import ProfileCard from '../../components/ProfileCard';
import { getUserName } from '../../store/auth/AuthSelectors';
import { getUserName } from '../../store/user/UserSelectors';
const CHAR_MAX = 100;
......
......@@ -4,7 +4,8 @@ import { StyleSheet, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import theme from '../../theme';
import { createCache, geoSearch } from '../../store/caches/CachesActions';
import { getAuthToken, getUserName } from '../../store/auth/AuthSelectors';
import { getAuthToken } from '../../store/auth/AuthSelectors';
import { getUserName } from '../../store/user/UserSelectors';
import { getPendingCacheContent, getPendingCacheDuration } from '../../store/pendingNewCache/PendingCacheSelectors';
import GeoButton from '../../components/GeoButton';
import { getCurrentPosition } from '../../store/location/LocationActions';
......
......@@ -19,10 +19,12 @@ type CameraDirection = 'front' | 'back';
const styles = StyleSheet.create({
buttonBar: {
display: 'flex',
position: 'absolute',
bottom: 0,
flex: 1,
flexDirection: 'row',
paddingVertical: theme.spacing.medium,
padding: theme.spacing.medium,
marginVertical: theme.spacing.medium,
marginHorizontal: theme.spacing.medium,
},
buttonContainer: {
flex: 1,
......@@ -87,7 +89,7 @@ const CameraModal = () => {
<GeoButton
title={'Retake'}
onPress={onRetake}
style={{ flex: 1, marginRight: theme.spacing.small }}
style={{ flex: 1, marginRight: theme.spacing.medium }}
/>
<GeoButton
title={'Done'}
......
......@@ -68,14 +68,12 @@ const GamesScreen: React.FC = () => {
style={{ marginTop: theme.spacing.large }}
/>
</View>
<View style={{ marginHorizontal: theme.spacing.screenPadding }}>
<GeoButton
title={'Next'}
onPress={() => navigate('CacheSubmit')}
disabled={!moveMade}
style={{ marginVertical: theme.spacing.large }}
/>
</View>
<GeoButton
title={'Next'}
onPress={() => navigate('CacheSubmit')}
disabled={!moveMade}
style={{ marginVertical: theme.spacing.large, marginHorizontal: theme.spacing.screenPadding }}
/>
</ScreenContainer>
);
};
......
......@@ -17,7 +17,7 @@ import GeoButton from '../../components/GeoButton';
import ScreenContainer from './ScreenContainer';
import { getPendingCacheImage } from '../../store/pendingNewCache/PendingCacheSelectors';
import ProfileCard from '../../components/ProfileCard';
import { getUserName } from '../../store/auth/AuthSelectors';
import { getUserName } from '../../store/user/UserSelectors';
const styles = StyleSheet.create({