Skip to content

Commit c9d77cb

Browse files
Merge pull request #60 from binarapps/chore/sections-division-ProfileScreen
FEAT: Profile screen
2 parents 5209c8e + 2e8b62d commit c9d77cb

29 files changed

+521
-242
lines changed

app.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
"softwareKeyboardLayoutMode": "pan",
77
"googleServicesFile": "./google-services.json"
88
},
9-
"assetBundlePatterns": [
10-
"**/*"
11-
],
9+
"assetBundlePatterns": ["**/*"],
1210
"ios": {
1311
"buildNumber": "20",
1412
"config": {
@@ -55,4 +53,4 @@
5553
"bundler": "metro"
5654
}
5755
}
58-
}
56+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"expo-device": "~6.0.2",
122122
"expo-font": "~12.0.5",
123123
"expo-haptics": "~13.0.1",
124+
"expo-image-picker": "~15.0.5",
124125
"expo-linear-gradient": "~13.0.2",
125126
"expo-linking": "~6.3.1",
126127
"expo-local-authentication": "~14.0.1",

src/components/FeaturedIcon.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Box, Icon } from '@baca/design-system'
2-
import { IconNames } from '@baca/types/icon'
2+
import { IconNames } from '@baca/types'
33

44
type FeatureIconSize = 'sm' | 'md' | 'lg' | 'xl'
55

src/components/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@ export * from './LanguagePicker'
1212
export * from './Modal'
1313
export * from './StatusBar'
1414
export * from './Version'
15+
16+
export * from './screens/profile/ProfileControlledInput'
17+
export * from './screens/profile/ProfileDeleteAccountButton'
18+
export * from './screens/profile/ProfileDetailsForm'
19+
export * from './screens/profile/ProfileEditImage'
20+
export * from './screens/profile/ProfileHeader'
21+
export * from './screens/profile/ProfilePasswordForm'
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ControlledField } from '@baca/components/organisms/ControlledField'
2+
import { isWeb } from '@baca/constants'
3+
import { Box, Text } from '@baca/design-system'
4+
import { useWeb } from '@baca/hooks'
5+
import { ProfileControlledInputProps } from '@baca/types'
6+
7+
export const ProfileControlledInput = ({
8+
label,
9+
name,
10+
isDisabled = false,
11+
...rest
12+
}: ProfileControlledInputProps) => {
13+
const { shouldApplyMobileStyles } = useWeb()
14+
15+
return (
16+
<Box
17+
justifyContent="space-between"
18+
flexDirection={isWeb && !shouldApplyMobileStyles ? 'row' : 'column'}
19+
mb={isWeb ? 10 : 0}
20+
maxW={800}
21+
>
22+
<Text.SmBold flex={1} color="text.primary" mb={2.5}>
23+
{label}
24+
</Text.SmBold>
25+
<Box flex={isWeb ? 2 : 0}>
26+
<ControlledField.Input
27+
autoCapitalize="none"
28+
inputMode={name === 'email' ? 'email' : 'text'}
29+
name={name}
30+
testID={`${name}Input`}
31+
isDisabled={isDisabled}
32+
{...rest}
33+
/>
34+
</Box>
35+
</Box>
36+
)
37+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { useAuthControllerDelete } from '@baca/api/query/auth/auth'
2+
import { Button, Text, Spacer, Row, Box, useBottomSheet } from '@baca/design-system'
3+
import { useCallback, useTranslation } from '@baca/hooks'
4+
import { signOut } from '@baca/store/auth'
5+
import { showErrorToast } from '@baca/utils'
6+
7+
export const ProfileDeleteAccountButton = () => {
8+
const { t } = useTranslation()
9+
const { mutateAsync: removeUserAccount, isLoading } = useAuthControllerDelete()
10+
11+
const { bottomSheetComponentRenderFunction, closeBottomSheet, presentBottomSheet } =
12+
useBottomSheet({
13+
title: '',
14+
isDivider: false,
15+
})
16+
17+
const handleRemoveUserAccount = useCallback(async () => {
18+
try {
19+
await removeUserAccount()
20+
signOut()
21+
} catch {
22+
showErrorToast({
23+
description: t('errors.something_went_wrong'),
24+
})
25+
}
26+
}, [removeUserAccount, t])
27+
28+
const bottomSheet = bottomSheetComponentRenderFunction(
29+
<Box px={4} pb={10}>
30+
<Text.LgSemibold color="text.primary" pt={4} pb={2}>
31+
{t('profile_screen.are_you_sure')}
32+
</Text.LgSemibold>
33+
<Text.SmRegular color="text.secondary" lineHeight="md">
34+
{t('profile_screen.remove_account_desc')}
35+
</Text.SmRegular>
36+
<Row w="full" justifyContent="space-between" pt={8}>
37+
<Button variant="SecondaryGray" flex={1} borderRadius={8} onPress={closeBottomSheet}>
38+
{t('common.cancel')}
39+
</Button>
40+
<Spacer x={3} />
41+
<Button
42+
onPress={handleRemoveUserAccount}
43+
variant="PrimaryDestructive"
44+
flex={1}
45+
borderRadius={8}
46+
loading={isLoading}
47+
>
48+
{t('profile_screen.remove_account')}
49+
</Button>
50+
</Row>
51+
</Box>,
52+
{
53+
name: 'delete-bin-line',
54+
color: 'featured.icon.light.fg.error',
55+
bgColor: 'bg.error.secondary',
56+
}
57+
)
58+
59+
return (
60+
<Box borderColor="border.secondary" borderTopWidth={1} py={6} alignItems="flex-start">
61+
<Button
62+
leftIconName="delete-bin-line"
63+
variant="SecondaryDestructive"
64+
borderRadius={8}
65+
onPress={presentBottomSheet}
66+
>
67+
{t('profile_screen.remove_account')}
68+
</Button>
69+
{bottomSheet}
70+
</Box>
71+
)
72+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Box, Button, Spacer, Row } from '@baca/design-system'
2+
import { useUpdateProfileForm } from '@baca/hooks'
3+
import { useRouter } from 'expo-router'
4+
import { useCallback } from 'react'
5+
import { useTranslation } from 'react-i18next'
6+
7+
import { ProfileControlledInput } from './ProfileControlledInput'
8+
9+
export const ProfileDetailsForm = () => {
10+
const { t } = useTranslation()
11+
const { control, errors, setFocus, submit, isSubmitting } = useUpdateProfileForm()
12+
const { back } = useRouter()
13+
14+
const focusLastNameInput = useCallback(() => setFocus('lastName'), [setFocus])
15+
16+
return (
17+
<Box>
18+
<Box borderColor="border.secondary" borderTopWidth={1} py={6}>
19+
<ProfileControlledInput
20+
label={t('form.labels.first_name')}
21+
name="firstName"
22+
placeholder={t('form.placeholders.first_name')}
23+
{...{ control, errors }}
24+
onFocus={focusLastNameInput}
25+
/>
26+
<ProfileControlledInput
27+
label={t('form.labels.last_name')}
28+
name="lastName"
29+
placeholder={t('form.placeholders.last_name')}
30+
{...{ control, errors }}
31+
/>
32+
<ProfileControlledInput
33+
label={t('form.labels.email')}
34+
name="email"
35+
placeholder={t('form.placeholders.email')}
36+
{...{ control, errors }}
37+
isDisabled
38+
onSubmitEditing={submit}
39+
/>
40+
<Row maxW={800} justifyContent="flex-end">
41+
<Button.SecondaryColor
42+
disabled={isSubmitting}
43+
loading={isSubmitting}
44+
onPress={back}
45+
testID="backProfileButton"
46+
>
47+
{t('common.cancel')}
48+
</Button.SecondaryColor>
49+
<Spacer x="4" />
50+
<Button
51+
disabled={isSubmitting}
52+
loading={isSubmitting}
53+
onPress={submit}
54+
testID="saveProfileUpdateButton"
55+
>
56+
{t('common.save')}
57+
</Button>
58+
</Row>
59+
</Box>
60+
</Box>
61+
)
62+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Box, Text, Button, Row, themeColors } from '@baca/design-system'
2+
import * as ImagePicker from 'expo-image-picker'
3+
import { t } from 'i18next'
4+
import { useState, useCallback } from 'react'
5+
import { Image, StyleSheet } from 'react-native'
6+
7+
export const ProfileEditImage: React.FC = () => {
8+
const [image, setImage] = useState<string | null>(null)
9+
10+
const pickImage = useCallback(async () => {
11+
const result = await ImagePicker.launchImageLibraryAsync({
12+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
13+
allowsEditing: true,
14+
aspect: [4, 3],
15+
quality: 1,
16+
})
17+
18+
if (!result.canceled && result.assets && result.assets.length > 0) {
19+
setImage(result.assets[0].uri)
20+
}
21+
}, [])
22+
23+
return (
24+
<Row style={styles.container}>
25+
<Box style={styles.textContainer}>
26+
<Text.SmBold color="text.primary">{t('profile_screen.your_photo')}</Text.SmBold>
27+
<Text.SmRegular color="text.secondary">
28+
{t('profile_screen.your_photo_description')}
29+
</Text.SmRegular>
30+
</Box>
31+
<Box style={styles.imageContainer}>
32+
{image ? (
33+
<Image source={{ uri: image }} style={styles.image} />
34+
) : (
35+
<Box style={styles.placeholder}>
36+
<Text fontSize={11} color="Gray modern.600">
37+
{t('profile_screen.photo_innerText')}
38+
</Text>
39+
</Box>
40+
)}
41+
</Box>
42+
<Button onPress={pickImage}>{t('common.upload')}</Button>
43+
</Row>
44+
)
45+
}
46+
47+
const styles = StyleSheet.create({
48+
container: {
49+
alignItems: 'flex-start',
50+
flexDirection: 'row',
51+
flexWrap: 'wrap',
52+
justifyContent: 'space-between',
53+
maxWidth: 800,
54+
},
55+
image: {
56+
height: '100%',
57+
width: '100%',
58+
},
59+
imageContainer: {
60+
borderRadius: 32,
61+
height: 64,
62+
marginBottom: 10,
63+
marginRight: 82,
64+
overflow: 'hidden',
65+
width: 64,
66+
},
67+
placeholder: {
68+
alignItems: 'center',
69+
backgroundColor: themeColors.primitives['Gray neutral']['50'],
70+
height: '100%',
71+
justifyContent: 'center',
72+
},
73+
textContainer: {
74+
flex: 1,
75+
marginBottom: 30,
76+
maxWidth: 260,
77+
minWidth: 260,
78+
},
79+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Box, Text } from '@baca/design-system'
2+
import { useTranslation } from '@baca/hooks'
3+
4+
export const ProfileHeader = () => {
5+
const { t } = useTranslation()
6+
return (
7+
<Box>
8+
<Text.LgBold color="text.primary">{t('profile_screen.profile')}</Text.LgBold>
9+
<Text.MdRegular color="text.secondary">
10+
{t('profile_screen.update_your_details')}
11+
</Text.MdRegular>
12+
</Box>
13+
)
14+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Box, Button, Row } from '@baca/design-system'
2+
import { useCallback } from '@baca/hooks'
3+
import { useUpdatePasswordForm } from '@baca/hooks/forms'
4+
import { usePasswordValidation } from '@baca/hooks/usePasswordValidation'
5+
import { useTranslation } from 'react-i18next'
6+
import { Keyboard } from 'react-native'
7+
8+
import { ProfileControlledInput } from './ProfileControlledInput'
9+
10+
export const ProfilePasswordForm = () => {
11+
const { t } = useTranslation()
12+
const { control, errors, submit, isSubmitting, setFocus } = useUpdatePasswordForm()
13+
14+
const { isPasswordError, passwordSuggestions, validationFn } = usePasswordValidation()
15+
16+
const focusNewPasswordInput = useCallback(() => setFocus('password'), [setFocus])
17+
18+
return (
19+
<Box borderColor="border.secondary" borderTopWidth={1} py={6}>
20+
<ProfileControlledInput
21+
control={control}
22+
errors={errors}
23+
label={t('form.labels.old_password')}
24+
name="oldPassword"
25+
onSubmitEditing={focusNewPasswordInput}
26+
placeholder={t('form.placeholders.old_password')}
27+
type="password"
28+
/>
29+
<ProfileControlledInput
30+
control={control}
31+
errors={{}}
32+
isInvalid={isPasswordError}
33+
isRequired
34+
label={t('form.labels.new_password')}
35+
name="password"
36+
onSubmitEditing={Keyboard.dismiss}
37+
placeholder={t('form.placeholders.new_password')}
38+
rules={{ validate: { validationFn } }}
39+
type="password"
40+
/>
41+
{passwordSuggestions}
42+
<Row maxW={800} justifyContent="flex-end">
43+
<Button
44+
disabled={isSubmitting}
45+
loading={isSubmitting}
46+
onPress={submit}
47+
testID="changePasswordButton"
48+
>
49+
{t('common.change')}
50+
</Button>
51+
</Row>
52+
</Box>
53+
)
54+
}

0 commit comments

Comments
 (0)