diff --git a/package-lock.json b/package-lock.json
index 38e5d3c9..23cd16ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,7 @@
"@mui/material": "^5.14.13",
"@mui/styled-engine-sc": "^6.0.0-alpha.1",
"@mui/system": "^5.14.13",
- "@react-native-async-storage/async-storage": "^1.18.2",
+ "@react-native-async-storage/async-storage": "1.18.2",
"@react-native-community/datetimepicker": "7.2.0",
"@react-navigation/bottom-tabs": "^6.5.9",
"@react-navigation/material-bottom-tabs": "^6.2.17",
@@ -38,6 +38,7 @@
"react": "18.2.0",
"react-native": "0.72.6",
"react-native-dom-parser": "^1.5.3",
+ "react-native-element-dropdown": "^2.10.0",
"react-native-elements": "^3.4.3",
"react-native-gesture-handler": "~2.12.0",
"react-native-htmlview": "^0.16.0",
@@ -4772,9 +4773,9 @@
}
},
"node_modules/@react-native-async-storage/async-storage": {
- "version": "1.19.5",
- "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.19.5.tgz",
- "integrity": "sha512-zLT7oNPXpW8BxJyHyq8AJbXtlHE/eonFWuJt44y0WeCGnp4KOJ8mfqD8mtAIKLyrYHHE1uadFe/s4C+diYAi8g==",
+ "version": "1.18.2",
+ "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.18.2.tgz",
+ "integrity": "sha512-dM8AfdoeIxlh+zqgr0o5+vCTPQ0Ru1mrPzONZMsr7ufp5h+6WgNxQNza7t0r5qQ6b04AJqTlBNixTWZxqP649Q==",
"dependencies": {
"merge-options": "^3.0.4"
},
@@ -16366,6 +16367,21 @@
"resolved": "https://registry.npmjs.org/react-native-dom-parser/-/react-native-dom-parser-1.5.3.tgz",
"integrity": "sha512-bcuNLVK2D0T39kRAK75/FJZFcvZ6/V3F0f5niLfYN+9gKelmNcCvBVHOOrEz4ZMmJ/1iutam8PqeF1fNqGV7Jg=="
},
+ "node_modules/react-native-element-dropdown": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/react-native-element-dropdown/-/react-native-element-dropdown-2.10.0.tgz",
+ "integrity": "sha512-aFjw0JbUIKGkqklHxDm8aDCZW6Q1GICP7pIHc0sllvpWpEamItzOwAMV4G1mBIKxED77Mp+nkq9p0Dhsr/faVw==",
+ "dependencies": {
+ "lodash": "^4.17.21"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-elements": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/react-native-elements/-/react-native-elements-3.4.3.tgz",
diff --git a/package.json b/package.json
index a495ae17..e1fa14d7 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"@mui/material": "^5.14.13",
"@mui/styled-engine-sc": "^6.0.0-alpha.1",
"@mui/system": "^5.14.13",
- "@react-native-async-storage/async-storage": "^1.18.2",
+ "@react-native-async-storage/async-storage": "1.18.2",
"@react-native-community/datetimepicker": "7.2.0",
"@react-navigation/bottom-tabs": "^6.5.9",
"@react-navigation/material-bottom-tabs": "^6.2.17",
@@ -42,6 +42,7 @@
"react": "18.2.0",
"react-native": "0.72.6",
"react-native-dom-parser": "^1.5.3",
+ "react-native-element-dropdown": "^2.10.0",
"react-native-elements": "^3.4.3",
"react-native-gesture-handler": "~2.12.0",
"react-native-htmlview": "^0.16.0",
@@ -55,10 +56,9 @@
"react-native-screens": "~3.22.0",
"react-native-svg": "13.9.0",
"react-native-url-polyfill": "^2.0.0",
- "validator": "^13.11.0",
"react-native-vector-icons": "^10.0.2",
"react-scroll-to-top": "^3.0.0",
- "expo-font": "~11.4.0"
+ "validator": "^13.11.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
diff --git a/src/app/auth/login/index.tsx b/src/app/auth/login/index.tsx
index d57f289d..d1b82dc7 100644
--- a/src/app/auth/login/index.tsx
+++ b/src/app/auth/login/index.tsx
@@ -6,11 +6,11 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import validator from 'validator';
import styles from './styles';
+import StyledButton from '../../../components/StyledButton/StyledButton';
+import UserStringInput from '../../../components/UserStringInput/UserStringInput';
import { isEmailUsed, queryEmailByUsername } from '../../../queries/auth';
import globalStyles from '../../../styles/globalStyles';
import { useSession } from '../../../utils/AuthContext';
-import StyledButton from '../../../components/StyledButton/StyledButton';
-import UserStringInput from '../../../components/UserStringInput/UserStringInput';
function LoginScreen() {
const sessionHandler = useSession();
diff --git a/src/app/auth/onboarding/index.tsx b/src/app/auth/onboarding/index.tsx
index 06ac3761..c18cf826 100644
--- a/src/app/auth/onboarding/index.tsx
+++ b/src/app/auth/onboarding/index.tsx
@@ -5,10 +5,10 @@ import { Alert, ScrollView, Platform } from 'react-native';
import { Button } from 'react-native-elements';
import styles from './styles';
+import StyledButton from '../../../components/StyledButton/StyledButton';
import UserStringInput from '../../../components/UserStringInput/UserStringInput';
import { useSession } from '../../../utils/AuthContext';
import supabase from '../../../utils/supabase';
-import StyledButton from '../../../components/StyledButton/StyledButton';
function OnboardingScreen() {
const { session } = useSession();
diff --git a/src/app/auth/signup/index.tsx b/src/app/auth/signup/index.tsx
index 6812b857..bb52638c 100644
--- a/src/app/auth/signup/index.tsx
+++ b/src/app/auth/signup/index.tsx
@@ -27,7 +27,7 @@ function SignUpScreen() {
const [password, setPassword] = useState('');
const [passwordTextHidden, setPasswordTextHidden] = useState(true);
const [loading, setLoading] = useState(false);
- let lastUsernameCheck = useRef(Date.now());
+ const lastUsernameCheck = useRef(Date.now());
const validUsernameCharacters = /^\w+$/g;
const [passwordComplexity, setPasswordComplexity] = useState(false);
@@ -295,8 +295,8 @@ function SignUpScreen() {
disabled={
!passwordComplexity ||
loading ||
- emailError != '' ||
- usernameError != '' ||
+ emailError !== '' ||
+ usernameError !== '' ||
email.length === 0 ||
username.length === 0
}
diff --git a/src/app/settings.tsx b/src/app/settings.tsx
deleted file mode 100644
index 133c866c..00000000
--- a/src/app/settings.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-import DateTimePicker from '@react-native-community/datetimepicker';
-import { Redirect, router } from 'expo-router';
-import { useEffect, useState } from 'react';
-import { Text, StyleSheet, View, Alert, Platform } from 'react-native';
-import { Button, Input } from 'react-native-elements';
-import { SafeAreaView } from 'react-native-safe-area-context';
-
-import StyledButton from '../components/StyledButton/StyledButton';
-import UserStringInput from '../components/UserStringInput/UserStringInput';
-import globalStyles from '../styles/globalStyles';
-import { useSession } from '../utils/AuthContext';
-import supabase from '../utils/supabase';
-
-function SettingsScreen() {
- const { session, signOut } = useSession();
- const [loading, setLoading] = useState(true);
- const [firstName, setFirstName] = useState('');
- const [lastName, setLastName] = useState('');
- const [birthday, setBirthday] = useState(new Date());
- const [gender, setGender] = useState('');
- const [raceEthnicity, setRaceEthnicity] = useState('');
- const [showDatePicker, setShowDatePicker] = useState(Platform.OS === 'ios');
-
- const getProfile = async () => {
- try {
- setLoading(true);
- if (!session?.user) throw new Error('No user on the session!');
-
- const { data, error, status } = await supabase
- .from('profiles')
- .select(`first_name, last_name, birthday, gender, race_ethnicity`)
- .eq('user_id', session?.user.id)
- .single();
-
- if (error && status !== 406 && error instanceof Error) {
- throw error;
- }
-
- if (data) {
- setFirstName(data.first_name || firstName);
- setLastName(data.last_name || lastName);
- setBirthday(new Date(data.birthday) || birthday);
- setGender(data.gender || gender);
- setRaceEthnicity(data.race_ethnicity || raceEthnicity);
- }
- } catch (error) {
- if (error instanceof Error) {
- Alert.alert(`Get profile error: ${error.message}`);
- }
- } finally {
- setLoading(false);
- }
- };
-
- const resetAndPushToRouter = (path: string) => {
- while (router.canGoBack()) {
- router.back();
- }
- router.replace(path);
- };
-
- useEffect(() => {
- if (session) getProfile();
- }, [session]);
-
- useEffect(() => {
- if (!session) resetAndPushToRouter('/auth/login');
- }, [session]);
-
- const updateProfile = async () => {
- try {
- setLoading(true);
- if (!session?.user) throw new Error('No user on the session!');
-
- // Only update values that are not blank
- const updates = {
- ...(firstName && { first_name: firstName }),
- ...(lastName && { last_name: lastName }),
- ...(gender && { gender }),
- ...(raceEthnicity && { race_ethnicity: raceEthnicity }),
- ...(birthday && { birthday }),
- };
-
- // Check if user exists
- const { count } = await supabase
- .from('profiles')
- .select(`*`, { count: 'exact' })
- .eq('user_id', session?.user.id);
-
- if (count && count >= 1) {
- // Update user if they exist
- const { error } = await supabase
- .from('profiles')
- .update(updates)
- .eq('user_id', session?.user.id)
- .select('*');
-
- if (error && error instanceof Error) throw error;
- } else {
- // Create user if they don't exist
- const { error } = await supabase.from('profiles').insert(updates);
-
- if (error && error instanceof Error) throw error;
- }
-
- Alert.alert('Succesfully updated account!');
- } catch (error) {
- if (error instanceof Error) {
- Alert.alert(error.message);
- }
- } finally {
- setLoading(false);
- }
- };
-
- if (!session) {
- return ;
- }
-
- return (
-
- Settings
-
-
-
-
-
-
- {Platform.OS !== 'ios' && (
-
- );
-}
-
-const styles = StyleSheet.create({
- verticallySpaced: {
- paddingTop: 4,
- paddingBottom: 4,
- alignSelf: 'stretch',
- },
-});
-
-export default SettingsScreen;
diff --git a/src/app/settings/_layout.tsx b/src/app/settings/_layout.tsx
new file mode 100644
index 00000000..d021a461
--- /dev/null
+++ b/src/app/settings/_layout.tsx
@@ -0,0 +1,11 @@
+import { Stack } from 'expo-router';
+
+function StackLayout() {
+ return (
+
+
+
+ );
+}
+
+export default StackLayout;
diff --git a/src/app/settings/index.tsx b/src/app/settings/index.tsx
new file mode 100644
index 00000000..792a8640
--- /dev/null
+++ b/src/app/settings/index.tsx
@@ -0,0 +1,242 @@
+import DateTimePicker from '@react-native-community/datetimepicker';
+import { Redirect, router, Link } from 'expo-router';
+import { useEffect, useState } from 'react';
+import { Text, View, Alert, Platform } from 'react-native';
+import { Button } from 'react-native-elements';
+import { ScrollView } from 'react-native-gesture-handler';
+import { SafeAreaView } from 'react-native-safe-area-context';
+
+import styles from './styles';
+import AccountDataDisplay from '../../components/AccountDataDisplay/AccountDataDisplay';
+import StyledButton from '../../components/StyledButton/StyledButton';
+import UserSelectorInput from '../../components/UserSelectorInput/UserSelectorInput';
+import globalStyles from '../../styles/globalStyles';
+import { useSession } from '../../utils/AuthContext';
+import supabase from '../../utils/supabase';
+
+function SettingsScreen() {
+ const { session, signOut } = useSession();
+ const [loading, setLoading] = useState(true);
+ const [firstName, setFirstName] = useState('');
+ const [username, setUsername] = useState('');
+ const [lastName, setLastName] = useState('');
+ const [pronouns, setPronouns] = useState('');
+ const [birthday, setBirthday] = useState(new Date());
+ const [gender, setGender] = useState('');
+ const [raceEthnicity, setRaceEthnicity] = useState('');
+
+ const [showSaveEdits, setShowSaveEdits] = useState(false);
+ const [showDatePicker, setShowDatePicker] = useState(Platform.OS === 'ios');
+
+ const wrapInDetectChange = (onChange: (_: any) => any) => {
+ return (value: any) => {
+ setShowSaveEdits(true);
+ return onChange(value);
+ };
+ };
+
+ const getProfile = async () => {
+ try {
+ setLoading(true);
+ if (!session?.user) throw new Error('No user on the session!');
+
+ const { data, error, status } = await supabase
+ .from('profiles')
+ .select(
+ `first_name, last_name, username, birthday, gender, race_ethnicity`,
+ )
+ .eq('user_id', session?.user.id)
+ .single();
+
+ if (error && status !== 406 && error instanceof Error) {
+ throw error;
+ }
+
+ if (data) {
+ setFirstName(data.first_name || firstName);
+ setLastName(data.last_name || lastName);
+ setUsername(data.username || username);
+
+ if (data.birthday) {
+ setBirthday(new Date(data.birthday));
+ setShowDatePicker(false);
+ } else {
+ setShowDatePicker(true);
+ }
+
+ setGender(data.gender || gender);
+ // setPronouns(data.pronouns || pronouns);
+ setRaceEthnicity(data.race_ethnicity || raceEthnicity);
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ Alert.alert(`Get profile error: ${error.message}`);
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const resetAndPushToRouter = (path: string) => {
+ while (router.canGoBack()) {
+ router.back();
+ }
+ router.replace(path);
+ };
+
+ useEffect(() => {
+ if (session) getProfile();
+ }, [session]);
+
+ useEffect(() => {
+ if (!session) resetAndPushToRouter('/auth/login');
+ }, [session]);
+
+ const updateProfile = async () => {
+ try {
+ setLoading(true);
+ if (!session?.user) throw new Error('No user on the session!');
+
+ // Only update values that are not blank
+ const updates = {
+ ...(firstName && { first_name: firstName }),
+ ...(lastName && { last_name: lastName }),
+ ...(gender && { gender }),
+ ...(pronouns && { pronouns }),
+ ...(raceEthnicity && { race_ethnicity: raceEthnicity }),
+ ...(birthday && { birthday }),
+ };
+
+ // Check if user exists
+ const { count } = await supabase
+ .from('profiles')
+ .select(`*`, { count: 'exact' })
+ .eq('user_id', session?.user.id);
+
+ if (count && count >= 1) {
+ // Update user if they exist
+ const { error } = await supabase
+ .from('profiles')
+ .update(updates)
+ .eq('user_id', session?.user.id)
+ .select('*');
+
+ if (error && error instanceof Error) throw error;
+ } else {
+ // Create user if they don't exist
+ const { error } = await supabase.from('profiles').insert(updates);
+
+ if (error && error instanceof Error) throw error;
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ Alert.alert(error.message);
+ }
+ } finally {
+ setLoading(false);
+ setShowSaveEdits(false);
+ }
+ };
+
+ if (!session) {
+ return ;
+ }
+
+ return (
+
+
+ {'
+
+
+
+
+ Settings
+ Account
+
+
+
+
+
+
+ {
+ setShowDatePicker(Platform.OS === 'ios');
+ if (date.nativeEvent.timestamp) {
+ setBirthday(new Date(date.nativeEvent.timestamp));
+ }
+ }}
+ />
+ {Platform.OS !== 'ios' && (
+
+ ) : (
+ birthday.toLocaleDateString().toString()
+ )
+ }
+ />
+
+
+
+
+
+
+
+ {showSaveEdits ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+export default SettingsScreen;
diff --git a/src/app/settings/styles.tsx b/src/app/settings/styles.tsx
new file mode 100644
index 00000000..762c5070
--- /dev/null
+++ b/src/app/settings/styles.tsx
@@ -0,0 +1,51 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ button: {
+ marginBottom: 32,
+ },
+ main: {
+ flex: 1,
+ width: '100%',
+ paddingLeft: 12,
+ justifyContent: 'space-between',
+ },
+ verticallySpaced: {
+ paddingTop: 4,
+ paddingBottom: 4,
+ alignSelf: 'stretch',
+ },
+ subheading: {
+ fontFamily: 'Manrope',
+ fontSize: 18,
+ fontStyle: 'normal',
+ fontWeight: '700',
+ paddingBottom: 16,
+ },
+ heading: {
+ paddingBottom: 24,
+ fontFamily: 'Manrope',
+ fontSize: 24,
+ fontStyle: 'normal',
+ fontWeight: '700',
+ },
+ back: {
+ paddingTop: 30,
+ paddingBottom: 16,
+ color: '#797979',
+ fontSize: 12,
+ fontWeight: '400',
+ },
+ staticData: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ alignItems: 'flex-start',
+ marginBottom: 6,
+ },
+ label: {
+ fontSize: 12,
+ fontFamily: 'Manrope',
+ fontStyle: 'normal',
+ fontWeight: '400',
+ },
+});
diff --git a/src/components/AccountDataDisplay/AccountDataDisplay.tsx b/src/components/AccountDataDisplay/AccountDataDisplay.tsx
new file mode 100644
index 00000000..a2d464f3
--- /dev/null
+++ b/src/components/AccountDataDisplay/AccountDataDisplay.tsx
@@ -0,0 +1,23 @@
+import { View, Text } from 'react-native';
+
+import styles from './styles';
+
+type AccountDataDisplayProps = {
+ label: string;
+ value: string | React.ReactNode;
+};
+
+function AccountDataDisplay({ label, value }: AccountDataDisplayProps) {
+ return (
+
+ {label}
+ {typeof value === 'string' ? (
+ {value}
+ ) : (
+ value
+ )}
+
+ );
+}
+
+export default AccountDataDisplay;
diff --git a/src/components/AccountDataDisplay/styles.tsx b/src/components/AccountDataDisplay/styles.tsx
new file mode 100644
index 00000000..26d911b6
--- /dev/null
+++ b/src/components/AccountDataDisplay/styles.tsx
@@ -0,0 +1,25 @@
+import { StyleSheet } from 'react-native';
+
+import colors from '../../styles/colors';
+
+export default StyleSheet.create({
+ view: {
+ width: '50%',
+ marginBottom: 26,
+ },
+ label: {
+ fontSize: 12,
+ fontFamily: 'Manrope',
+ fontStyle: 'normal',
+ fontWeight: '400',
+ color: colors.textGrey,
+ },
+ value: {
+ paddingTop: 18,
+ paddingRight: 20,
+ fontSize: 14,
+ fontFamily: 'Manrope',
+ fontStyle: 'normal',
+ fontWeight: '400',
+ },
+});
diff --git a/src/components/UserSelectorInput/UserSelectorInput.tsx b/src/components/UserSelectorInput/UserSelectorInput.tsx
new file mode 100644
index 00000000..d9eb3ddd
--- /dev/null
+++ b/src/components/UserSelectorInput/UserSelectorInput.tsx
@@ -0,0 +1,65 @@
+import { View, Text } from 'react-native';
+import { Dropdown } from 'react-native-element-dropdown';
+import { Icon } from 'react-native-elements';
+
+import styles from './styles';
+
+type UserSelectorInputProps = {
+ options: string[];
+ label: string;
+ placeholder: string;
+ setValue: (value: string) => any;
+ value: string;
+};
+
+type Option = {
+ label: string;
+ value: string;
+};
+
+function UserSelectorInput({
+ options,
+ label,
+ placeholder,
+ setValue,
+ value,
+}: UserSelectorInputProps) {
+ return (
+
+ {label}
+
+ {
+ return { label: option, value: option };
+ })}
+ maxHeight={400}
+ labelField="label"
+ valueField="value"
+ placeholder={placeholder}
+ value={value}
+ renderItem={(item: Option, selected: boolean | undefined) => (
+ {item.value}
+ )}
+ renderRightIcon={() => (
+
+ )}
+ onChange={(item: Option) => {
+ setValue(item.value);
+ }}
+ />
+
+
+ );
+}
+
+export default UserSelectorInput;
diff --git a/src/components/UserSelectorInput/styles.tsx b/src/components/UserSelectorInput/styles.tsx
new file mode 100644
index 00000000..ad458a2c
--- /dev/null
+++ b/src/components/UserSelectorInput/styles.tsx
@@ -0,0 +1,51 @@
+import { StyleSheet } from 'react-native';
+
+import colors from '../../styles/colors';
+
+export default StyleSheet.create({
+ outer: {
+ position: 'relative',
+ zIndex: 1,
+ },
+ container: {
+ marginBottom: 16,
+ },
+ label: {
+ fontSize: 12,
+ fontStyle: 'normal',
+ fontWeight: '400',
+ color: colors.textGrey,
+ marginBottom: 10,
+ },
+ dropdown: {
+ height: 50,
+ borderWidth: 1,
+ borderRadius: 5,
+ paddingHorizontal: 10,
+ },
+ dropdownContainer: {
+ borderRadius: 5,
+ borderWidth: 1,
+ borderColor: colors.shadowDark,
+ borderTopWidth: 0,
+ borderTopEndRadius: 0,
+ borderTopStartRadius: 0,
+ position: 'relative',
+ top: -6,
+ },
+ itemContainer: {
+ paddingHorizontal: 5,
+ paddingVertical: 5,
+ borderRadius: 5,
+ },
+ icon: {
+ marginRight: 5,
+ },
+ textStyle: {
+ fontSize: 14,
+ },
+ iconStyle: {
+ width: 20,
+ height: 20,
+ },
+});
diff --git a/src/components/UserStringInput/UserStringInput.tsx b/src/components/UserStringInput/UserStringInput.tsx
index f345729e..7c7a2456 100644
--- a/src/components/UserStringInput/UserStringInput.tsx
+++ b/src/components/UserStringInput/UserStringInput.tsx
@@ -9,6 +9,7 @@ type UserStringInputProps = {
attributes?: TextInput['props'] | null;
children?: ReactNode;
label?: string;
+ labelColor?: string;
onChange?: (text: string) => any;
};
@@ -18,11 +19,14 @@ export default function UserStringInput({
attributes = {},
label,
children,
+ labelColor = '#000',
onChange = _ => {},
}: UserStringInputProps) {
return (
- {label && {label}}
+ {label && (
+ {label}
+ )}
onChange(e.nativeEvent.text)}
diff --git a/src/components/UserStringInput/styles.tsx b/src/components/UserStringInput/styles.tsx
index e5c28efc..ffefb4fc 100644
--- a/src/components/UserStringInput/styles.tsx
+++ b/src/components/UserStringInput/styles.tsx
@@ -3,6 +3,7 @@ import { StyleSheet } from 'react-native';
export default StyleSheet.create({
mt16: {
marginTop: 16,
+ width: '100%',
},
label: {
fontSize: 12,
diff --git a/src/styles/globalStyles.ts b/src/styles/globalStyles.ts
index 57d49c3f..50e6a079 100644
--- a/src/styles/globalStyles.ts
+++ b/src/styles/globalStyles.ts
@@ -6,8 +6,7 @@ export default StyleSheet.create({
backgroundColor: 'white',
alignItems: 'flex-start',
justifyContent: 'flex-start',
- paddingLeft: 24,
- paddingRight: 24,
+ paddingHorizontal: 24,
},
authContainer: {
marginHorizontal: 38,