├── .expo-shared ├── assets.json └── README.md ├── assets ├── Logo.png ├── icon.png ├── splash.png ├── favicon.png ├── Logo-splash.png ├── adaptive-icon.png └── fonts │ ├── Karla-Bold.ttf │ ├── Karla-Medium.ttf │ ├── Karla-ExtraBold.ttf │ ├── Karla-Regular.ttf │ ├── MarkaziText-Medium.ttf │ └── MarkaziText-Regular.ttf ├── img ├── restauranfood.png └── littleLemonLogo.png ├── little-lemon-wireframe.jpg ├── contexts └── AuthContext.js ├── babel.config.js ├── .gitignore ├── utils ├── index.js └── utils.js ├── eas.json ├── screens ├── SplashScreen.js ├── Onboarding.js ├── Home.js └── Profile.js ├── package.json ├── app.json ├── components └── Filters.js ├── database.js ├── App.js └── README.md /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /assets/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/Logo.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/splash.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/Logo-splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/Logo-splash.png -------------------------------------------------------------------------------- /img/restauranfood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/img/restauranfood.png -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /img/littleLemonLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/img/littleLemonLogo.png -------------------------------------------------------------------------------- /assets/fonts/Karla-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/fonts/Karla-Bold.ttf -------------------------------------------------------------------------------- /little-lemon-wireframe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/little-lemon-wireframe.jpg -------------------------------------------------------------------------------- /assets/fonts/Karla-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/fonts/Karla-Medium.ttf -------------------------------------------------------------------------------- /contexts/AuthContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export const AuthContext = createContext(); 4 | -------------------------------------------------------------------------------- /assets/fonts/Karla-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/fonts/Karla-ExtraBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Karla-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/fonts/Karla-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/MarkaziText-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/fonts/MarkaziText-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/MarkaziText-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marventures/little-lemon-app/HEAD/assets/fonts/MarkaziText-Regular.ttf -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | export const validateEmail = email => { 2 | return email.match( 3 | /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 4 | ); 5 | }; 6 | 7 | export const validateName = name => { 8 | return name.match(/^[a-zA-Z]+$/); 9 | }; 10 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 10.1.0" 4 | }, 5 | "build": { 6 | "development": { 7 | "developmentClient": true, 8 | "distribution": "internal", 9 | "channel": "development" 10 | }, 11 | "preview": { 12 | "distribution": "internal", 13 | "channel": "preview" 14 | }, 15 | "production": { 16 | "channel": "production" 17 | } 18 | }, 19 | "submit": { 20 | "production": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.expo-shared/README.md: -------------------------------------------------------------------------------- 1 | > Why do I have a folder named ".expo-shared" in my project? 2 | 3 | The ".expo-shared" folder is created when running commands that produce state that is intended to be shared with all developers on the project. For example, "npx expo-optimize". 4 | 5 | > What does the "assets.json" file contain? 6 | 7 | The "assets.json" file describes the assets that have been optimized through "expo-optimize" and do not need to be processed again. 8 | 9 | > Should I commit the ".expo-shared" folder? 10 | 11 | Yes, you should share the ".expo-shared" folder with your collaborators. 12 | -------------------------------------------------------------------------------- /screens/SplashScreen.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, StyleSheet, Image } from "react-native"; 3 | 4 | const SplashScreen = () => { 5 | return ( 6 | 7 | 11 | 12 | ); 13 | }; 14 | 15 | const styles = StyleSheet.create({ 16 | container: { 17 | flex: 1, 18 | backgroundColor: "#fff", 19 | justifyContent: "center", 20 | alignItems: "center", 21 | }, 22 | logo: { 23 | height: 100, 24 | width: "90%", 25 | resizeMode: "contain", 26 | }, 27 | }); 28 | 29 | export default SplashScreen; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "little-lemon-app", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@react-native-async-storage/async-storage": "~1.17.3", 13 | "@react-navigation/native": "^6.1.2", 14 | "@react-navigation/native-stack": "^6.9.8", 15 | "expo": "~47.0.12", 16 | "expo-checkbox": "~2.2.2", 17 | "expo-font": "~11.0.1", 18 | "expo-image-picker": "~14.0.2", 19 | "expo-splash-screen": "~0.17.5", 20 | "expo-sqlite": "~11.0.0", 21 | "expo-status-bar": "~1.4.2", 22 | "expo-updates": "~0.15.6", 23 | "lodash.debounce": "^4.0.8", 24 | "react": "18.1.0", 25 | "react-native": "0.70.8", 26 | "react-native-pager-view": "6.0.1", 27 | "react-native-paper": "^5.1.3", 28 | "react-native-safe-area-context": "4.4.1", 29 | "react-native-screens": "~3.18.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.12.9", 33 | "@types/react-native": "^0.73.0" 34 | }, 35 | "private": true 36 | } 37 | -------------------------------------------------------------------------------- /utils/utils.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react"; 2 | 3 | export function getSectionListData(data) { 4 | let restructured = []; 5 | data.map(item => { 6 | let obj = restructured.find( 7 | x => 8 | x.name == item.category.charAt(0).toUpperCase() + item.category.slice(1) 9 | ); 10 | if (obj) { 11 | restructured[restructured.indexOf(obj)].data.push({ 12 | id: item.id, 13 | name: item.name, 14 | price: item.price, 15 | description: item.description, 16 | image: item.image, 17 | }); 18 | } else { 19 | restructured.push({ 20 | name: item.category.charAt(0).toUpperCase() + item.category.slice(1), 21 | data: [ 22 | { 23 | id: item.id, 24 | name: item.name, 25 | price: item.price, 26 | description: item.description, 27 | image: item.image, 28 | }, 29 | ], 30 | }); 31 | } 32 | }); 33 | return restructured; 34 | } 35 | 36 | export function useUpdateEffect(effect, dependencies = []) { 37 | const isInitialMount = useRef(true); 38 | 39 | useEffect(() => { 40 | if (isInitialMount.current) { 41 | isInitialMount.current = false; 42 | } else { 43 | return effect(); 44 | } 45 | }, dependencies); 46 | } 47 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "little-lemon-app", 4 | "owner": "marventures", 5 | "slug": "little-lemon-app", 6 | "description": "This application is a React Native Food App. Users will have to go through a registration process. Once they successfully complete that phase, they are redirected to a home screen where they can search and filter menu items. User can also change their preferences in Profile page", 7 | "version": "1.0.0", 8 | "orientation": "portrait", 9 | "icon": "./assets/Logo.png", 10 | "userInterfaceStyle": "light", 11 | "splash": { 12 | "image": "./assets/Logo-splash.png", 13 | "resizeMode": "contain", 14 | "backgroundColor": "#ffffff" 15 | }, 16 | "updates": { 17 | "fallbackToCacheTimeout": 0, 18 | "url": "https://u.expo.dev/221d2bc0-e34d-4538-b828-2e1cabe5bba7" 19 | }, 20 | "assetBundlePatterns": [ 21 | "**/*" 22 | ], 23 | "ios": { 24 | "supportsTablet": true 25 | }, 26 | "android": { 27 | "adaptiveIcon": { 28 | "foregroundImage": "./assets/Logo.png", 29 | "backgroundColor": "#FFFFFF" 30 | } 31 | }, 32 | "web": { 33 | "favicon": "./assets/Logo.png" 34 | }, 35 | "extra": { 36 | "eas": { 37 | "projectId": "221d2bc0-e34d-4538-b828-2e1cabe5bba7" 38 | } 39 | }, 40 | "runtimeVersion": { 41 | "policy": "sdkVersion" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /components/Filters.js: -------------------------------------------------------------------------------- 1 | import { View, TouchableOpacity, Text, StyleSheet } from "react-native"; 2 | 3 | const Filters = ({ onChange, selections, sections }) => { 4 | return ( 5 | 6 | {sections.map((section, index) => ( 7 | { 10 | onChange(index); 11 | }} 12 | style={{ 13 | flex: 1 / sections.length, 14 | justifyContent: "center", 15 | alignItems: "center", 16 | padding: 16, 17 | backgroundColor: selections[index] ? "#495e57" : "#edefee", 18 | borderRadius: 9, 19 | marginRight: 15, 20 | }} 21 | > 22 | 23 | 29 | {section.charAt(0).toUpperCase() + section.slice(1)} 30 | 31 | 32 | 33 | ))} 34 | 35 | ); 36 | }; 37 | 38 | const styles = StyleSheet.create({ 39 | filtersContainer: { 40 | backgroundColor: "#fff", 41 | flexDirection: "row", 42 | alignItems: "center", 43 | marginBottom: 16, 44 | paddingLeft: 15, 45 | }, 46 | }); 47 | 48 | export default Filters; 49 | -------------------------------------------------------------------------------- /database.js: -------------------------------------------------------------------------------- 1 | import * as SQLite from "expo-sqlite"; 2 | 3 | const db = SQLite.openDatabase("little_lemon"); 4 | 5 | export async function createTable() { 6 | return new Promise((resolve, reject) => { 7 | db.transaction( 8 | tx => { 9 | tx.executeSql( 10 | "create table if not exists menuitems (id integer primary key not null, name text, price text, description text, image text, category text);" 11 | ); 12 | }, 13 | reject, 14 | resolve 15 | ); 16 | }); 17 | } 18 | 19 | export async function getMenuItems() { 20 | return new Promise(resolve => { 21 | db.transaction(tx => { 22 | tx.executeSql("select * from menuitems", [], (_, { rows }) => { 23 | resolve(rows._array); 24 | }); 25 | }); 26 | }); 27 | } 28 | 29 | export function saveMenuItems(menuItems) { 30 | db.transaction(tx => { 31 | tx.executeSql( 32 | `insert into menuitems (id, name, price, description, image, category) values ${menuItems 33 | .map( 34 | item => 35 | `("${item.id}", "${item.name}", "${item.price}", "${item.description}", "${item.image}", "${item.category}")` 36 | ) 37 | .join(", ")}` 38 | ); 39 | }); 40 | } 41 | 42 | export async function filterByQueryAndCategories(query, activeCategories) { 43 | return new Promise((resolve, reject) => { 44 | db.transaction(tx => { 45 | tx.executeSql( 46 | `select * from menuitems where name like ? and category in ('${activeCategories.join( 47 | "','" 48 | )}')`, 49 | [`%${query}%`], 50 | (_, { rows }) => { 51 | resolve(rows._array); 52 | } 53 | ); 54 | }, reject); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import { NavigationContainer } from "@react-navigation/native"; 2 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 3 | import { useEffect, useMemo, useReducer } from "react"; 4 | import { Alert } from "react-native"; 5 | import { Onboarding } from "./screens/Onboarding"; 6 | import { Profile } from "./screens/Profile"; 7 | import SplashScreen from "./screens/SplashScreen"; 8 | import { Home } from "./screens/Home"; 9 | import { StatusBar } from "expo-status-bar"; 10 | 11 | import AsyncStorage from "@react-native-async-storage/async-storage"; 12 | 13 | import { AuthContext } from "./contexts/AuthContext"; 14 | 15 | const Stack = createNativeStackNavigator(); 16 | 17 | export default function App({ navigation }) { 18 | const [state, dispatch] = useReducer( 19 | (prevState, action) => { 20 | switch (action.type) { 21 | case "onboard": 22 | return { 23 | ...prevState, 24 | isLoading: false, 25 | isOnboardingCompleted: action.isOnboardingCompleted, 26 | }; 27 | } 28 | }, 29 | { 30 | isLoading: true, 31 | isOnboardingCompleted: false, 32 | } 33 | ); 34 | 35 | useEffect(() => { 36 | (async () => { 37 | let profileData = []; 38 | try { 39 | const getProfile = await AsyncStorage.getItem("profile"); 40 | if (getProfile !== null) { 41 | profileData = getProfile; 42 | } 43 | } catch (e) { 44 | console.error(e); 45 | } finally { 46 | if (Object.keys(profileData).length != 0) { 47 | dispatch({ type: "onboard", isOnboardingCompleted: true }); 48 | } else { 49 | dispatch({ type: "onboard", isOnboardingCompleted: false }); 50 | } 51 | } 52 | })(); 53 | }, []); 54 | 55 | const authContext = useMemo( 56 | () => ({ 57 | onboard: async (data) => { 58 | try { 59 | const jsonValue = JSON.stringify(data); 60 | await AsyncStorage.setItem("profile", jsonValue); 61 | } catch (e) { 62 | console.error(e); 63 | } 64 | 65 | dispatch({ type: "onboard", isOnboardingCompleted: true }); 66 | }, 67 | update: async (data) => { 68 | try { 69 | const jsonValue = JSON.stringify(data); 70 | await AsyncStorage.setItem("profile", jsonValue); 71 | } catch (e) { 72 | console.error(e); 73 | } 74 | 75 | Alert.alert("Success", "Successfully saved changes!"); 76 | }, 77 | logout: async () => { 78 | try { 79 | await AsyncStorage.clear(); 80 | } catch (e) { 81 | console.error(e); 82 | } 83 | 84 | dispatch({ type: "onboard", isOnboardingCompleted: false }); 85 | }, 86 | }), 87 | [] 88 | ); 89 | 90 | if (state.isLoading) { 91 | return ; 92 | } 93 | 94 | return ( 95 | 96 | 97 | 98 | 99 | {state.isOnboardingCompleted ? ( 100 | <> 101 | 106 | 107 | 108 | ) : ( 109 | 114 | )} 115 | 116 | 117 | 118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Little Lemon Food Ordering App 2 | 3 | - The application is a React Native Expo Food app. 4 | - Users will be capable of signing up on the Little Lemon restaurant app. 5 | - Users will have to go through a registration process. 6 | - Once they successfully complete that phase, they are redirected to a home screen. 7 | - Home screen will represent the landing screen after completing the onboarding flow, displaying a header, a banner with a search bar and a list of menu items where a user can filter each categories. 8 | - User can also customize their name, email, photo and and other user preferences through a Profile Screen. 9 | - Profile screen also contains four checkboxes enable specific email notifications, like order status, password changes,special offers, and newsletters. 10 | - Use AsyncStorage module to preserve the chosen preferences even when the user quits the application 11 | - When clicking the Logout button, user will redirect back to login page, clearing all data saved from Profile. 12 | - Use SQLite Database to populate, query and filter menu items. 13 | 14 | ## Table of contents 15 | 16 | - [Overview](#overview) 17 | - [How to use the project](#how-to-use-the-project) 18 | - [Screenshot](#screenshot) 19 | - [Links](#links) 20 | - [My process](#my-process) 21 | - [Built with](#built-with) 22 | - [What I learned](#what-i-learned) 23 | - [Useful resources](#useful-resources) 24 | - [Author](#author) 25 | 26 | ## Overview 27 | 28 | ### How to use the project 29 | 30 | ##### npm install && npm start 31 | 32 | ##### Then, a QR Code wil appear on your terminal. 33 | 34 | ##### On IOS Scan QR code through Camera app. 35 | 36 | ##### On Android : Scan QR code through Expo Go app. 37 | 38 | ##### You can also scan this [QR CODE](https://expo.dev/preview/update?message=Publish%20Update&updateRuntimeVersion=exposdk%3A47.0.0&createdAt=2024-07-03T09%3A21%3A32.588Z&slug=exp&projectId=221d2bc0-e34d-4538-b828-2e1cabe5bba7&group=2468e4a0-6270-4a2e-8e34-17167031fde8) to view the project. (Please use [SDK Version 47](https://expo.dev/go) on Expo Go to view the project) 39 | 40 | ### Screenshot 41 | 42 | ![final_mockup](https://user-images.githubusercontent.com/108392678/217717918-a6f83c94-c1ab-4796-903e-388b9a67cdd9.jpg) 43 | ![Onboarding](https://user-images.githubusercontent.com/108392678/217715066-19026169-ab51-450e-b21c-cc925940d03e.jpg) 44 | ![Profile and Home](https://user-images.githubusercontent.com/108392678/217715079-d66eb960-f5cf-4cdf-8f33-b45b320fca7e.jpg) 45 | 46 | ### Links 47 | 48 | - Github: [Code](https://github.com/marventures/little-lemon-app) 49 | - Demo : Scan the [QR Code](https://expo.dev/preview/update?message=Publish%20Update&updateRuntimeVersion=exposdk%3A47.0.0&createdAt=2024-07-03T09%3A21%3A32.588Z&slug=exp&projectId=221d2bc0-e34d-4538-b828-2e1cabe5bba7&group=2468e4a0-6270-4a2e-8e34-17167031fde8) to see the demo. 50 | 51 | ## My process 52 | 53 | ### Built with 54 | 55 | - [React Native](https://reactnative.dev/docs/environment-setup) - React Native app built with expo 56 | - [SQLite](https://docs.expo.dev/versions/latest/sdk/sqlite/) - For storing restaurant's menu items. 57 | - [AsyncStorage](https://react-native-async-storage.github.io/async-storage/docs/api/) - For storing user preferences. 58 | - [StyleSheet](https://reactnative.dev/docs/stylesheet) - For styles 59 | 60 | ### What I learned 61 | 62 | - Create a React Native App using Expo 63 | - Create a wireframe and high fidelity mockup using Figma. 64 | - Use ContextAPI for login 65 | - Use React Navigation (Native Stack) for screen routes. 66 | - Use ImagePicker API to set user Profile Picture 67 | - Use useFonts Hook from expo-fonts to set custom fonts 68 | - Use AsyncStorage to store user settings. 69 | - Use getItem and setItem methods to read and set data to AsyncStorage 70 | - ConnectAsyncStorage to a state 71 | - Use SQLite to store Menu Items 72 | - Connect SQLite to a state 73 | - Create form validation for users 74 | - Handling side-effects using useEffect Hook 75 | - Use FlatList component to render menu 76 | - Use ScrollView component to render categories title 77 | - Use View, View, Text Components 78 | - Extract all styles to StyleSheet API 79 | 80 | Here is a code snippet: 81 | 82 | ```jsx 83 | const [profile, setProfile] = useState({ 84 | firstName: "", 85 | lastName: "", 86 | email: "", 87 | phoneNumber: "", 88 | orderStatuses: false, 89 | passwordChanges: false, 90 | specialOffers: false, 91 | newsletter: false, 92 | image: "", 93 | }); 94 | const [data, setData] = useState([]); 95 | const [searchBarText, setSearchBarText] = useState(""); 96 | const [query, setQuery] = useState(""); 97 | const [filterSelections, setFilterSelections] = useState( 98 | sections.map(() => false) 99 | ); 100 | 101 | const fetchData = async () => { 102 | try { 103 | const response = await fetch(API_URL); 104 | const json = await response.json(); 105 | const menu = json.menu.map((item, index) => ({ 106 | id: index + 1, 107 | name: item.name, 108 | price: item.price.toString(), 109 | description: item.description, 110 | image: item.image, 111 | category: item.category, 112 | })); 113 | return menu; 114 | } catch (error) { 115 | console.error(error); 116 | } finally { 117 | } 118 | }; 119 | 120 | useEffect(() => { 121 | (async () => { 122 | let menuItems = []; 123 | try { 124 | await createTable(); 125 | menuItems = await getMenuItems(); 126 | if (!menuItems.length) { 127 | menuItems = await fetchData(); 128 | saveMenuItems(menuItems); 129 | } 130 | const sectionListData = getSectionListData(menuItems); 131 | setData(sectionListData); 132 | const getProfile = await AsyncStorage.getItem("profile"); 133 | setProfile(JSON.parse(getProfile)); 134 | } catch (e) { 135 | Alert.alert(e.message); 136 | } 137 | })(); 138 | }, []); 139 | ``` 140 | 141 | ### Useful resources 142 | 143 | - [React Native Docs (StyleSheet) ](https://reactnative.dev/docs/stylesheet) - This helped me for all the neccessary React Native styles. I really liked their documentation and will use it going forward. 144 | - [ImagePicker API](https://docs.expo.dev/versions/latest/sdk/imagepicker/) - This helped me for creating an option for user to select profile picture on their devices. 145 | - [SQLite](https://docs.expo.dev/versions/latest/sdk/sqlite/) - This helped me for saving menu items. 146 | - [Async Storage](https://react-native-async-storage.github.io/async-storage/docs/api/) - This helped me for saving user settings. 147 | - [ContextAPI](https://beta.reactjs.org/reference/react/createContext)- This helped me for creating a authentication context for login. 148 | 149 | ## Author 150 | 151 | - Website - [Marvin Morales Pacis](https://marvin-morales-pacis.vercel.app/) 152 | - LinkedIn - [@marventures](https://www.linkedin.com/in/marventures/) 153 | - Twitter - [@marventures11](https://www.twitter.com/marventures11) 154 | -------------------------------------------------------------------------------- /screens/Onboarding.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useContext, useCallback } from "react"; 2 | // prettier-ignore 3 | import { View, Image, StyleSheet, Text, KeyboardAvoidingView, Platform, TextInput, Pressable} from "react-native"; 4 | import PagerView from "react-native-pager-view"; 5 | import { validateEmail, validateName } from "../utils"; 6 | import Constants from "expo-constants"; 7 | 8 | import { AuthContext } from "../contexts/AuthContext"; 9 | import { useFonts } from "expo-font"; 10 | import * as SplashScreen from "expo-splash-screen"; 11 | 12 | export const Onboarding = () => { 13 | const [firstName, onChangeFirstName] = useState(""); 14 | const [lastName, onChangeLastName] = useState(""); 15 | const [email, onChangeEmail] = useState(""); 16 | 17 | const isEmailValid = validateEmail(email); 18 | const isFirstNameValid = validateName(firstName); 19 | const isLastNameValid = validateName(lastName); 20 | const viewPagerRef = useRef(PagerView); 21 | 22 | const { onboard } = useContext(AuthContext); 23 | 24 | // FONTS 25 | const [fontsLoaded] = useFonts({ 26 | "Karla-Regular": require("../assets/fonts/Karla-Regular.ttf"), 27 | "Karla-Medium": require("../assets/fonts/Karla-Medium.ttf"), 28 | "Karla-Bold": require("../assets/fonts/Karla-Bold.ttf"), 29 | "Karla-ExtraBold": require("../assets/fonts/Karla-ExtraBold.ttf"), 30 | "MarkaziText-Regular": require("../assets/fonts/MarkaziText-Regular.ttf"), 31 | "MarkaziText-Medium": require("../assets/fonts/MarkaziText-Medium.ttf"), 32 | }); 33 | 34 | const onLayoutRootView = useCallback(async () => { 35 | if (fontsLoaded) { 36 | await SplashScreen.hideAsync(); 37 | } 38 | }, [fontsLoaded]); 39 | 40 | if (!fontsLoaded) { 41 | return null; 42 | } 43 | 44 | return ( 45 | 50 | 51 | 57 | 58 | Let us get to know you 59 | 65 | 66 | 67 | First Name 68 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | viewPagerRef.current.setPage(1)} 83 | disabled={!isFirstNameValid} 84 | > 85 | Next 86 | 87 | 88 | 89 | 90 | Last Name 91 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | viewPagerRef.current.setPage(0)} 107 | > 108 | Back 109 | 110 | viewPagerRef.current.setPage(2)} 116 | disabled={!isLastNameValid} 117 | > 118 | Next 119 | 120 | 121 | 122 | 123 | 124 | Email 125 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | viewPagerRef.current.setPage(1)} 142 | > 143 | Back 144 | 145 | onboard({ firstName, lastName, email })} 148 | disabled={!isEmailValid} 149 | > 150 | Submit 151 | 152 | 153 | 154 | 155 | 156 | ); 157 | }; 158 | 159 | const styles = StyleSheet.create({ 160 | container: { 161 | flex: 1, 162 | backgroundColor: "#fff", 163 | paddingTop: Constants.statusBarHeight, 164 | }, 165 | header: { 166 | padding: 12, 167 | flexDirection: "row", 168 | justifyContent: "center", 169 | backgroundColor: "#dee3e9", 170 | }, 171 | logo: { 172 | height: 50, 173 | width: 150, 174 | resizeMode: "contain", 175 | }, 176 | viewPager: { 177 | flex: 1, 178 | }, 179 | page: { 180 | justifyContent: "center", 181 | }, 182 | pageContainer: { 183 | flex: 1, 184 | justifyContent: "center", 185 | alignItems: "center", 186 | }, 187 | welcomeText: { 188 | fontSize: 40, 189 | paddingVertical: 60, 190 | fontFamily: "MarkaziText-Medium", 191 | color: "#495E57", 192 | textAlign: "center", 193 | }, 194 | text: { 195 | fontSize: 24, 196 | fontFamily: "Karla-ExtraBold", 197 | color: "#495E57", 198 | }, 199 | inputBox: { 200 | borderColor: "#EDEFEE", 201 | backgroundColor: "#EDEFEE", 202 | alignSelf: "stretch", 203 | height: 50, 204 | margin: 18, 205 | borderWidth: 1, 206 | padding: 10, 207 | fontSize: 20, 208 | borderRadius: 9, 209 | fontFamily: "Karla-Medium", 210 | }, 211 | btn: { 212 | backgroundColor: "#f4ce14", 213 | borderColor: "#f4ce14", 214 | borderRadius: 9, 215 | alignSelf: "stretch", 216 | marginHorizontal: 18, 217 | marginBottom: 60, 218 | padding: 10, 219 | borderWidth: 1, 220 | }, 221 | btnDisabled: { 222 | backgroundColor: "#f1f4f7", 223 | }, 224 | buttons: { 225 | display: "flex", 226 | flexDirection: "row", 227 | justifyContent: "space-between", 228 | alignItems: "center", 229 | marginLeft: 18, 230 | marginBottom: 60, 231 | }, 232 | halfBtn: { 233 | flex: 1, 234 | borderColor: "#f4ce14", 235 | backgroundColor: "#f4ce14", 236 | borderRadius: 9, 237 | alignSelf: "stretch", 238 | marginRight: 18, 239 | padding: 10, 240 | borderWidth: 1, 241 | }, 242 | btntext: { 243 | fontSize: 22, 244 | color: "#333", 245 | fontFamily: "Karla-Bold", 246 | alignSelf: "center", 247 | }, 248 | pageIndicator: { 249 | display: "flex", 250 | flexDirection: "row", 251 | justifyContent: "space-between", 252 | alignItems: "center", 253 | justifyContent: "center", 254 | marginBottom: 20, 255 | }, 256 | pageDot: { 257 | backgroundColor: "#67788a", 258 | width: 22, 259 | height: 22, 260 | marginHorizontal: 10, 261 | borderRadius: 11, 262 | }, 263 | pageDotActive: { 264 | backgroundColor: "#f4ce14", 265 | width: 22, 266 | height: 22, 267 | borderRadius: 11, 268 | }, 269 | }); 270 | -------------------------------------------------------------------------------- /screens/Home.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useCallback, useMemo } from "react"; 2 | // prettier-ignore 3 | import {Text, View, StyleSheet, SectionList, Alert, Image, Pressable} from "react-native"; 4 | import { Searchbar } from "react-native-paper"; 5 | import debounce from "lodash.debounce"; 6 | // prettier-ignore 7 | import { createTable, getMenuItems,saveMenuItems, filterByQueryAndCategories} from "../database"; 8 | import Filters from "../components/Filters"; 9 | import { getSectionListData, useUpdateEffect } from "../utils/utils"; 10 | import AsyncStorage from "@react-native-async-storage/async-storage"; 11 | import Constants from "expo-constants"; 12 | import { useFonts } from "expo-font"; 13 | import * as SplashScreen from "expo-splash-screen"; 14 | 15 | const BASE_URL = 16 | "https://raw.githubusercontent.com/Meta-Mobile-Developer-PC/Working-With-Data-API/main/capstone.json"; 17 | 18 | const sections = ["starters", "mains", "desserts"]; 19 | 20 | const Item = ({ name, price, description, image }) => ( 21 | 22 | 23 | {name} 24 | {description} 25 | ${price} 26 | 27 | 33 | 34 | ); 35 | 36 | export const Home = ({ navigation }) => { 37 | const [profile, setProfile] = useState({ 38 | firstName: "", 39 | lastName: "", 40 | email: "", 41 | phoneNumber: "", 42 | orderStatuses: false, 43 | passwordChanges: false, 44 | specialOffers: false, 45 | newsletter: false, 46 | image: "", 47 | }); 48 | const [data, setData] = useState([]); 49 | const [searchBarText, setSearchBarText] = useState(""); 50 | const [query, setQuery] = useState(""); 51 | const [filterSelections, setFilterSelections] = useState( 52 | sections.map(() => false) 53 | ); 54 | 55 | const fetchData = async () => { 56 | try { 57 | const response = await fetch(BASE_URL); 58 | const json = await response.json(); 59 | const menu = json.menu.map((item, index) => ({ 60 | id: index + 1, 61 | name: item.name, 62 | price: item.price.toString(), 63 | description: item.description, 64 | image: item.image, 65 | category: item.category, 66 | })); 67 | return menu; 68 | } catch (error) { 69 | console.error(error); 70 | } finally { 71 | } 72 | }; 73 | 74 | useEffect(() => { 75 | (async () => { 76 | let menuItems = []; 77 | try { 78 | await createTable(); 79 | menuItems = await getMenuItems(); 80 | if (!menuItems.length) { 81 | menuItems = await fetchData(); 82 | saveMenuItems(menuItems); 83 | } 84 | const sectionListData = getSectionListData(menuItems); 85 | setData(sectionListData); 86 | const getProfile = await AsyncStorage.getItem("profile"); 87 | setProfile(JSON.parse(getProfile)); 88 | } catch (e) { 89 | Alert.alert(e.message); 90 | } 91 | })(); 92 | }, []); 93 | 94 | useUpdateEffect(() => { 95 | (async () => { 96 | const activeCategories = sections.filter((s, i) => { 97 | if (filterSelections.every((item) => item === false)) { 98 | return true; 99 | } 100 | return filterSelections[i]; 101 | }); 102 | try { 103 | const menuItems = await filterByQueryAndCategories( 104 | query, 105 | activeCategories 106 | ); 107 | const sectionListData = getSectionListData(menuItems); 108 | setData(sectionListData); 109 | } catch (e) { 110 | Alert.alert(e.message); 111 | } 112 | })(); 113 | }, [filterSelections, query]); 114 | 115 | const lookup = useCallback((q) => { 116 | setQuery(q); 117 | }, []); 118 | 119 | const debouncedLookup = useMemo(() => debounce(lookup, 1000), [lookup]); 120 | 121 | const handleSearchChange = (text) => { 122 | setSearchBarText(text); 123 | debouncedLookup(text); 124 | }; 125 | 126 | const handleFiltersChange = async (index) => { 127 | const arrayCopy = [...filterSelections]; 128 | arrayCopy[index] = !filterSelections[index]; 129 | setFilterSelections(arrayCopy); 130 | }; 131 | 132 | // FONTS 133 | const [fontsLoaded] = useFonts({ 134 | "Karla-Regular": require("../assets/fonts/Karla-Regular.ttf"), 135 | "Karla-Medium": require("../assets/fonts/Karla-Medium.ttf"), 136 | "Karla-Bold": require("../assets/fonts/Karla-Bold.ttf"), 137 | "Karla-ExtraBold": require("../assets/fonts/Karla-ExtraBold.ttf"), 138 | "MarkaziText-Regular": require("../assets/fonts/MarkaziText-Regular.ttf"), 139 | "MarkaziText-Medium": require("../assets/fonts/MarkaziText-Medium.ttf"), 140 | }); 141 | 142 | const onLayoutRootView = useCallback(async () => { 143 | if (fontsLoaded) { 144 | await SplashScreen.hideAsync(); 145 | } 146 | }, [fontsLoaded]); 147 | 148 | if (!fontsLoaded) { 149 | return null; 150 | } 151 | 152 | return ( 153 | 154 | 155 | 161 | navigation.navigate("Profile")} 164 | > 165 | {profile.image ? ( 166 | 167 | ) : ( 168 | 169 | 170 | {profile.firstName && Array.from(profile.firstName)[0]} 171 | {profile.lastName && Array.from(profile.lastName)[0]} 172 | 173 | 174 | )} 175 | 176 | 177 | 178 | Little Lemon 179 | 180 | 181 | Chicago 182 | 183 | We are a family owned Mediterranean restaurant, focused on 184 | traditional recipes served with a modern twist. 185 | 186 | 187 | 193 | 194 | 204 | 205 | ORDER FOR DELIVERY! 206 | 211 | item.id} 215 | renderItem={({ item }) => ( 216 | 222 | )} 223 | renderSectionHeader={({ section: { name } }) => ( 224 | {name} 225 | )} 226 | /> 227 | 228 | ); 229 | }; 230 | 231 | const styles = StyleSheet.create({ 232 | container: { 233 | flex: 1, 234 | backgroundColor: "#fff", 235 | paddingTop: Constants.statusBarHeight, 236 | }, 237 | header: { 238 | padding: 12, 239 | flexDirection: "row", 240 | justifyContent: "center", 241 | backgroundColor: "#dee3e9", 242 | }, 243 | logo: { 244 | height: 50, 245 | width: 150, 246 | resizeMode: "contain", 247 | }, 248 | sectionList: { 249 | paddingHorizontal: 16, 250 | }, 251 | searchBar: { 252 | marginTop: 15, 253 | backgroundColor: "#e4e4e4", 254 | shadowRadius: 0, 255 | shadowOpacity: 0, 256 | }, 257 | item: { 258 | flexDirection: "row", 259 | justifyContent: "space-between", 260 | alignItems: "center", 261 | borderTopWidth: 1, 262 | borderTopColor: "#cccccc", 263 | paddingVertical: 10, 264 | }, 265 | itemBody: { 266 | flex: 1, 267 | }, 268 | itemHeader: { 269 | fontSize: 24, 270 | paddingVertical: 8, 271 | color: "#495e57", 272 | backgroundColor: "#fff", 273 | fontFamily: "Karla-ExtraBold", 274 | }, 275 | name: { 276 | fontSize: 20, 277 | color: "#000000", 278 | paddingBottom: 5, 279 | fontFamily: "Karla-Bold", 280 | }, 281 | description: { 282 | color: "#495e57", 283 | paddingRight: 5, 284 | fontFamily: "Karla-Medium", 285 | }, 286 | price: { 287 | fontSize: 20, 288 | color: "#EE9972", 289 | paddingTop: 5, 290 | fontFamily: "Karla-Medium", 291 | }, 292 | itemImage: { 293 | width: 100, 294 | height: 100, 295 | }, 296 | avatar: { 297 | flex: 1, 298 | position: "absolute", 299 | right: 10, 300 | top: 10, 301 | }, 302 | avatarImage: { 303 | width: 50, 304 | height: 50, 305 | borderRadius: 25, 306 | }, 307 | avatarEmpty: { 308 | width: 50, 309 | height: 50, 310 | borderRadius: 25, 311 | backgroundColor: "#0b9a6a", 312 | alignItems: "center", 313 | justifyContent: "center", 314 | }, 315 | heroSection: { 316 | backgroundColor: "#495e57", 317 | padding: 15, 318 | }, 319 | heroHeader: { 320 | color: "#f4ce14", 321 | fontSize: 54, 322 | fontFamily: "MarkaziText-Medium", 323 | }, 324 | heroHeader2: { 325 | color: "#fff", 326 | fontSize: 30, 327 | fontFamily: "MarkaziText-Medium", 328 | }, 329 | heroText: { 330 | color: "#fff", 331 | fontFamily: "Karla-Medium", 332 | fontSize: 14, 333 | }, 334 | heroBody: { 335 | flexDirection: "row", 336 | justifyContent: "space-between", 337 | }, 338 | heroContent: { 339 | flex: 1, 340 | }, 341 | heroImage: { 342 | width: 100, 343 | height: 100, 344 | borderRadius: 12, 345 | }, 346 | delivery: { 347 | fontSize: 18, 348 | padding: 15, 349 | fontFamily: "Karla-ExtraBold", 350 | }, 351 | }); 352 | -------------------------------------------------------------------------------- /screens/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext, useCallback } from "react"; 2 | // prettier-ignore 3 | import { View, Image, StyleSheet, Text, KeyboardAvoidingView, Platform, TextInput, Pressable, ScrollView } from "react-native"; 4 | import { validateEmail } from "../utils"; 5 | import { AuthContext } from "../contexts/AuthContext"; 6 | import Checkbox from "expo-checkbox"; 7 | import AsyncStorage from "@react-native-async-storage/async-storage"; 8 | import * as ImagePicker from "expo-image-picker"; 9 | import { useFonts } from "expo-font"; 10 | import * as SplashScreen from "expo-splash-screen"; 11 | 12 | export const Profile = () => { 13 | const [profile, setProfile] = useState({ 14 | firstName: "", 15 | lastName: "", 16 | email: "", 17 | phoneNumber: "", 18 | orderStatuses: false, 19 | passwordChanges: false, 20 | specialOffers: false, 21 | newsletter: false, 22 | image: "", 23 | }); 24 | const [discard, setDiscard] = useState(false); 25 | 26 | useEffect(() => { 27 | (async () => { 28 | try { 29 | const getProfile = await AsyncStorage.getItem("profile"); 30 | setProfile(JSON.parse(getProfile)); 31 | setDiscard(false); 32 | } catch (e) { 33 | console.error(e); 34 | } 35 | })(); 36 | }, [discard]); 37 | 38 | const validateName = (name) => { 39 | if (name.length > 0) { 40 | return name.match(/[^a-zA-Z]/); 41 | } else { 42 | return true; 43 | } 44 | }; 45 | 46 | const validateNumber = (number) => { 47 | if (isNaN(number)) { 48 | return false; 49 | } else if (number.length == 10) { 50 | return true; 51 | } 52 | }; 53 | 54 | const { update } = useContext(AuthContext); 55 | const { logout } = useContext(AuthContext); 56 | 57 | const updateProfile = (key, value) => { 58 | setProfile((prevState) => ({ 59 | ...prevState, 60 | [key]: value, 61 | })); 62 | }; 63 | 64 | // FONTS 65 | const [fontsLoaded] = useFonts({ 66 | "Karla-Regular": require("../assets/fonts/Karla-Regular.ttf"), 67 | "Karla-Medium": require("../assets/fonts/Karla-Medium.ttf"), 68 | "Karla-Bold": require("../assets/fonts/Karla-Bold.ttf"), 69 | "Karla-ExtraBold": require("../assets/fonts/Karla-ExtraBold.ttf"), 70 | "MarkaziText-Regular": require("../assets/fonts/MarkaziText-Regular.ttf"), 71 | "MarkaziText-Medium": require("../assets/fonts/MarkaziText-Medium.ttf"), 72 | }); 73 | 74 | const onLayoutRootView = useCallback(async () => { 75 | if (fontsLoaded) { 76 | await SplashScreen.hideAsync(); 77 | } 78 | }, [fontsLoaded]); 79 | 80 | if (!fontsLoaded) { 81 | return null; 82 | } 83 | 84 | const getIsFormValid = () => { 85 | return ( 86 | !validateName(profile.firstName) && 87 | !validateName(profile.lastName) && 88 | validateEmail(profile.email) && 89 | validateNumber(profile.phoneNumber) 90 | ); 91 | }; 92 | 93 | const pickImage = async () => { 94 | let result = await ImagePicker.launchImageLibraryAsync({ 95 | mediaTypes: ImagePicker.MediaTypeOptions.All, 96 | allowsEditing: true, 97 | aspect: [4, 3], 98 | quality: 1, 99 | }); 100 | 101 | if (!result.canceled) { 102 | setProfile((prevState) => ({ 103 | ...prevState, 104 | ["image"]: result.assets[0].uri, 105 | })); 106 | } 107 | }; 108 | 109 | const removeImage = () => { 110 | setProfile((prevState) => ({ 111 | ...prevState, 112 | ["image"]: "", 113 | })); 114 | }; 115 | 116 | return ( 117 | 122 | 123 | 129 | 130 | 131 | Personal information 132 | Avatar 133 | 134 | {profile.image ? ( 135 | 136 | ) : ( 137 | 138 | 139 | {profile.firstName && Array.from(profile.firstName)[0]} 140 | {profile.lastName && Array.from(profile.lastName)[0]} 141 | 142 | 143 | )} 144 | 145 | 150 | Change 151 | 152 | 157 | Remove 158 | 159 | 160 | 161 | 167 | First Name 168 | 169 | updateProfile("firstName", newValue)} 173 | placeholder={"First Name"} 174 | /> 175 | 181 | Last Name 182 | 183 | updateProfile("lastName", newValue)} 187 | placeholder={"Last Name"} 188 | /> 189 | 195 | Email 196 | 197 | updateProfile("email", newValue)} 202 | placeholder={"Email"} 203 | /> 204 | 210 | Phone number (10 digit) 211 | 212 | updateProfile("phoneNumber", newValue)} 217 | placeholder={"Phone number"} 218 | /> 219 | Email notifications 220 | 221 | 225 | updateProfile("orderStatuses", newValue) 226 | } 227 | color={"#495e57"} 228 | /> 229 | Order statuses 230 | 231 | 232 | 236 | updateProfile("passwordChanges", newValue) 237 | } 238 | color={"#495e57"} 239 | /> 240 | Password changes 241 | 242 | 243 | 247 | updateProfile("specialOffers", newValue) 248 | } 249 | color={"#495e57"} 250 | /> 251 | Special offers 252 | 253 | 254 | updateProfile("newsletter", newValue)} 258 | color={"#495e57"} 259 | /> 260 | Newsletter 261 | 262 | logout()}> 263 | Log out 264 | 265 | 266 | setDiscard(true)}> 267 | Discard changes 268 | 269 | update(profile)} 272 | disabled={!getIsFormValid()} 273 | > 274 | Save changes 275 | 276 | 277 | 278 | 279 | ); 280 | }; 281 | 282 | const styles = StyleSheet.create({ 283 | container: { 284 | flex: 1, 285 | backgroundColor: "#fff", 286 | }, 287 | header: { 288 | padding: 12, 289 | flexDirection: "row", 290 | justifyContent: "center", 291 | backgroundColor: "#dee3e9", 292 | }, 293 | logo: { 294 | height: 50, 295 | width: 150, 296 | resizeMode: "contain", 297 | }, 298 | viewScroll: { 299 | flex: 1, 300 | padding: 10, 301 | }, 302 | headertext: { 303 | fontSize: 22, 304 | paddingBottom: 10, 305 | fontFamily: "Karla-ExtraBold", 306 | }, 307 | text: { 308 | fontSize: 16, 309 | marginBottom: 5, 310 | fontFamily: "Karla-Medium", 311 | }, 312 | inputBox: { 313 | alignSelf: "stretch", 314 | marginBottom: 10, 315 | borderWidth: 1, 316 | padding: 10, 317 | fontSize: 16, 318 | borderRadius: 9, 319 | borderColor: "#dfdfe5", 320 | }, 321 | btn: { 322 | backgroundColor: "#f4ce14", 323 | borderRadius: 9, 324 | alignSelf: "stretch", 325 | marginVertical: 18, 326 | padding: 10, 327 | borderWidth: 1, 328 | borderColor: "#cc9a22", 329 | }, 330 | btnDisabled: { 331 | backgroundColor: "#98b3aa", 332 | }, 333 | buttons: { 334 | display: "flex", 335 | flexDirection: "row", 336 | justifyContent: "space-between", 337 | alignItems: "center", 338 | marginBottom: 60, 339 | }, 340 | saveBtn: { 341 | flex: 1, 342 | backgroundColor: "#495e57", 343 | borderRadius: 9, 344 | alignSelf: "stretch", 345 | padding: 10, 346 | borderWidth: 1, 347 | borderColor: "#3f554d", 348 | }, 349 | saveBtnText: { 350 | fontSize: 18, 351 | color: "#FFFFFF", 352 | alignSelf: "center", 353 | fontFamily: "Karla-Bold", 354 | }, 355 | discardBtn: { 356 | flex: 1, 357 | backgroundColor: "#FFFFFF", 358 | borderRadius: 9, 359 | alignSelf: "stretch", 360 | marginRight: 18, 361 | padding: 10, 362 | borderWidth: 1, 363 | borderColor: "#83918c", 364 | }, 365 | discardBtnText: { 366 | fontSize: 18, 367 | color: "#3e524b", 368 | alignSelf: "center", 369 | fontFamily: "Karla-Bold", 370 | }, 371 | btntext: { 372 | fontSize: 22, 373 | color: "#3e524b", 374 | fontFamily: "Karla-Bold", 375 | alignSelf: "center", 376 | }, 377 | section: { 378 | flexDirection: "row", 379 | alignItems: "center", 380 | }, 381 | paragraph: { 382 | fontSize: 15, 383 | }, 384 | checkbox: { 385 | margin: 8, 386 | }, 387 | error: { 388 | color: "#d14747", 389 | fontWeight: "bold", 390 | }, 391 | avatarContainer: { 392 | flexDirection: "row", 393 | alignItems: "center", 394 | marginVertical: 10, 395 | }, 396 | avatarImage: { 397 | width: 80, 398 | height: 80, 399 | borderRadius: 40, 400 | }, 401 | avatarEmpty: { 402 | width: 80, 403 | height: 80, 404 | borderRadius: 40, 405 | backgroundColor: "#0b9a6a", 406 | alignItems: "center", 407 | justifyContent: "center", 408 | }, 409 | avatarEmptyText: { 410 | fontSize: 32, 411 | color: "#FFFFFF", 412 | fontWeight: "bold", 413 | }, 414 | avatarButtons: { 415 | flexDirection: "row", 416 | }, 417 | changeBtn: { 418 | backgroundColor: "#495e57", 419 | borderRadius: 9, 420 | marginHorizontal: 18, 421 | padding: 10, 422 | borderWidth: 1, 423 | borderColor: "#3f554d", 424 | }, 425 | removeBtn: { 426 | backgroundColor: "#FFFFFF", 427 | borderRadius: 9, 428 | padding: 10, 429 | borderWidth: 1, 430 | borderColor: "#83918c", 431 | }, 432 | }); 433 | --------------------------------------------------------------------------------