├── .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 | 
43 | 
44 | 
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 |
--------------------------------------------------------------------------------