├── .env ├── assets ├── data │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ ├── c1.png │ ├── c2.png │ ├── c3.png │ ├── c4.png │ ├── c5.png │ ├── c6.png │ ├── r1.jpeg │ ├── r2.jpeg │ ├── r3.jpeg │ ├── filter.json │ ├── home.ts │ └── restaurant.ts ├── images │ ├── bike.png │ ├── icon.png │ ├── favicon.png │ ├── splash.png │ └── adaptive-icon.png └── fonts │ └── SpaceMono-Regular.ttf ├── expo-env.d.ts ├── tsconfig.json ├── babel.config.js ├── Readme.md ├── metro.config.js ├── constants └── Colors.ts ├── .gitignore ├── app.json ├── app ├── index.tsx ├── (modal) │ ├── location-search.tsx │ ├── dish.tsx │ └── filter.tsx ├── _layout.tsx ├── basket.tsx └── details.tsx ├── Components ├── Categories.tsx ├── SwipeableRow.tsx ├── Restaurants.tsx ├── CustomHeader.tsx ├── BottomSheet.tsx └── ParallaxScrollView.js ├── store └── basketStore.ts └── package.json /.env: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_GOOGLE_API_KEY=YOURKEY -------------------------------------------------------------------------------- /assets/data/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/1.png -------------------------------------------------------------------------------- /assets/data/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/10.png -------------------------------------------------------------------------------- /assets/data/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/2.png -------------------------------------------------------------------------------- /assets/data/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/3.png -------------------------------------------------------------------------------- /assets/data/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/4.png -------------------------------------------------------------------------------- /assets/data/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/5.png -------------------------------------------------------------------------------- /assets/data/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/6.png -------------------------------------------------------------------------------- /assets/data/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/7.png -------------------------------------------------------------------------------- /assets/data/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/8.png -------------------------------------------------------------------------------- /assets/data/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/9.png -------------------------------------------------------------------------------- /assets/data/c1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/c1.png -------------------------------------------------------------------------------- /assets/data/c2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/c2.png -------------------------------------------------------------------------------- /assets/data/c3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/c3.png -------------------------------------------------------------------------------- /assets/data/c4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/c4.png -------------------------------------------------------------------------------- /assets/data/c5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/c5.png -------------------------------------------------------------------------------- /assets/data/c6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/c6.png -------------------------------------------------------------------------------- /assets/data/r1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/r1.jpeg -------------------------------------------------------------------------------- /assets/data/r2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/r2.jpeg -------------------------------------------------------------------------------- /assets/data/r3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/data/r3.jpeg -------------------------------------------------------------------------------- /assets/images/bike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/images/bike.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/images/splash.png -------------------------------------------------------------------------------- /expo-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // NOTE: This file should not be edited and should be in your git ignore -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/clone-deliveroo-react-native/HEAD/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | }, 9 | "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | // Required for expo-router 7 | 'expo-router/babel', 8 | 'react-native-reanimated/plugin', 9 | ], 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # React Native Food Ordering Clone 2 | 3 | This project contains all files from the Deliveroo Food Ordering UI clone tutorial. 4 | 5 | Screenshots and information were taken from the [official Deliveroo app](https://deliveroo.co.uk/) and don't belong to me. 6 | 7 | Find the [full tutorial on YouTube](https://youtu.be/FXnnCrfiNGM)! 8 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require('expo/metro-config'); 3 | 4 | /** @type {import('expo/metro-config').MetroConfig} */ 5 | const config = getDefaultConfig(__dirname, { 6 | // [Web-only]: Enables CSS support in Metro. 7 | isCSSEnabled: true, 8 | }); 9 | 10 | module.exports = config; 11 | -------------------------------------------------------------------------------- /constants/Colors.ts: -------------------------------------------------------------------------------- 1 | const tintColorLight = '#2f95dc'; 2 | const tintColorDark = '#fff'; 3 | 4 | export default { 5 | light: { 6 | text: '#000', 7 | background: '#fff', 8 | tint: tintColorLight, 9 | tabIconDefault: '#ccc', 10 | tabIconSelected: tintColorLight, 11 | }, 12 | dark: { 13 | text: '#fff', 14 | background: '#000', 15 | tint: tintColorDark, 16 | tabIconDefault: '#ccc', 17 | tabIconSelected: tintColorDark, 18 | }, 19 | primary: '#20E1B2', 20 | lightGrey: '#FCF8FF', 21 | grey: '#EEE9F0', 22 | medium: '#9F9AA1', 23 | mediumDark: '#424242', 24 | green: '#437919', 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 38 | # The following patterns were generated by expo-cli 39 | 40 | expo-env.d.ts 41 | # @end expo-cli 42 | 43 | .vscode/ -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "deliverooClone", 4 | "slug": "deliverooClone", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "assetBundlePatterns": ["**/*"], 16 | "ios": { 17 | "supportsTablet": true 18 | }, 19 | "android": { 20 | "adaptiveIcon": { 21 | "foregroundImage": "./assets/images/adaptive-icon.png", 22 | "backgroundColor": "#ffffff" 23 | } 24 | }, 25 | "web": { 26 | "bundler": "metro", 27 | "output": "static", 28 | "favicon": "./assets/images/favicon.png" 29 | }, 30 | "plugins": ["expo-router"], 31 | "experiments": { 32 | "typedRoutes": true, 33 | "tsconfigPaths": true 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text, ScrollView, StyleSheet } from 'react-native'; 2 | import React from 'react'; 3 | import Categories from '../Components/Categories'; 4 | import { SafeAreaView } from 'react-native-safe-area-context'; 5 | import Restaurants from '../Components/Restaurants'; 6 | import Colors from '@/constants/Colors'; 7 | 8 | const Page = () => { 9 | return ( 10 | 11 | 12 | 13 | Top picks in your neighbourhood 14 | 15 | Offers near you 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | const styles = StyleSheet.create({ 23 | container: { 24 | top: 50, 25 | backgroundColor: Colors.lightGrey, 26 | }, 27 | header: { 28 | fontSize: 18, 29 | fontWeight: 'bold', 30 | marginTop: 16, 31 | marginBottom: 8, 32 | paddingHorizontal: 16, 33 | }, 34 | }); 35 | 36 | export default Page; 37 | -------------------------------------------------------------------------------- /Components/Categories.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, ScrollView, StyleSheet, Image } from 'react-native'; 2 | import React from 'react'; 3 | import { categories } from '@/assets/data/home'; 4 | 5 | const Categories = () => { 6 | return ( 7 | 13 | {categories.map((category, index) => ( 14 | 15 | 16 | {category.text} 17 | 18 | ))} 19 | 20 | ); 21 | }; 22 | const styles = StyleSheet.create({ 23 | categoryCard: { 24 | width: 100, 25 | height: 100, 26 | backgroundColor: '#fff', 27 | marginEnd: 10, 28 | elevation: 2, 29 | shadowColor: '#000', 30 | shadowOffset: { 31 | width: 0, 32 | height: 4, 33 | }, 34 | shadowOpacity: 0.06, 35 | borderRadius: 4, 36 | }, 37 | categoryText: { 38 | padding: 6, 39 | fontSize: 14, 40 | fontWeight: 'bold', 41 | }, 42 | }); 43 | 44 | export default Categories; 45 | -------------------------------------------------------------------------------- /assets/data/filter.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Acai", 4 | "count": 7 5 | }, 6 | { 7 | "name": "African", 8 | "count": 9 9 | }, 10 | { 11 | "name": "Alcohol", 12 | "count": 330 13 | }, 14 | { 15 | "name": "American", 16 | "count": 201 17 | }, 18 | { 19 | "name": "BBQ", 20 | "count": 73 21 | }, 22 | { 23 | "name": "Brunch", 24 | "count": 118 25 | }, 26 | { 27 | "name": "Cakes", 28 | "count": 36 29 | }, 30 | { 31 | "name": "Dessert", 32 | "count": 282 33 | }, 34 | { 35 | "name": "Falafel", 36 | "count": 30 37 | }, 38 | { 39 | "name": "German", 40 | "count": 5 41 | }, 42 | { 43 | "name": "Healthy", 44 | "count": 247 45 | }, 46 | { 47 | "name": "Indian", 48 | "count": 112 49 | }, 50 | { 51 | "name": "Meal Deal", 52 | "count": 129 53 | }, 54 | { 55 | "name": "Pancakes", 56 | "count": 15 57 | }, 58 | { 59 | "name": "Pizza", 60 | "count": 208 61 | }, 62 | { 63 | "name": "Soup", 64 | "count": 8 65 | }, 66 | { 67 | "name": "Takeaways", 68 | "count": 87 69 | }, 70 | { 71 | "name": "Wraps", 72 | "count": 99 73 | } 74 | ] -------------------------------------------------------------------------------- /assets/data/home.ts: -------------------------------------------------------------------------------- 1 | export const categories = [ 2 | { 3 | text: 'Restaurants', 4 | img: require('@/assets/data/c1.png'), 5 | }, 6 | { 7 | text: 'Grocery', 8 | img: require('@/assets/data/c2.png'), 9 | }, 10 | { 11 | text: 'Offers', 12 | img: require('@/assets/data/c3.png'), 13 | }, 14 | { 15 | text: 'Pickup', 16 | img: require('@/assets/data/c4.png'), 17 | }, 18 | { 19 | text: 'HOP', 20 | img: require('@/assets/data/c5.png'), 21 | }, 22 | { 23 | text: 'Pharmacy', 24 | img: require('@/assets/data/c6.png'), 25 | }, 26 | ]; 27 | 28 | export const restaurants = [ 29 | { 30 | name: 'Vapiano', 31 | rating: '4.5 Excellent', 32 | ratings: '(500+)', 33 | distance: '0.7 miles away', 34 | img: require('@/assets/data/r1.jpeg'), 35 | tags: ['Italian', 'Pizza', 'Pasta', 'Salads'], 36 | duration: '35 - 45', 37 | }, 38 | { 39 | name: '✨Urban Greens✨', 40 | id: '2', 41 | rating: '4.9 Excellent', 42 | ratings: '(500+)', 43 | distance: '1.7 miles away', 44 | img: require('@/assets/data/r2.jpeg'), 45 | tags: ['Salads', 'Vegan', 'Healthy', 'British'], 46 | duration: '15 - 30', 47 | }, 48 | { 49 | name: 'El Minero', 50 | id: '3', 51 | rating: '4.5 Excellent', 52 | ratings: '(500+)', 53 | distance: '3 miles away', 54 | img: require('@/assets/data/r3.jpeg'), 55 | tags: ['Spanish', 'Salads', 'Tpas', 'Pasta'], 56 | duration: '25 - 45', 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /store/basketStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | export interface Product { 4 | id: number; 5 | name: string; 6 | price: number; 7 | info: string; 8 | img: any; 9 | } 10 | 11 | export interface BasketState { 12 | products: Array; 13 | addProduct: (product: Product) => void; 14 | reduceProduct: (product: Product) => void; 15 | clearCart: () => void; 16 | items: number; 17 | total: number; 18 | } 19 | 20 | const useBasketStore = create()((set) => ({ 21 | products: [], 22 | items: 0, 23 | total: 0, 24 | addProduct: (product) => { 25 | set((state) => { 26 | state.items += 1; 27 | state.total += product.price; 28 | const hasProduct = state.products.find((p) => p.id === product.id); 29 | 30 | if (hasProduct) { 31 | hasProduct.quantity += 1; 32 | return { products: [...state.products] }; 33 | } else { 34 | return { products: [...state.products, { ...product, quantity: 1 }] }; 35 | } 36 | }); 37 | }, 38 | reduceProduct: (product) => { 39 | set((state) => { 40 | state.total -= product.price; 41 | state.items -= 1; 42 | return { 43 | products: state.products 44 | .map((p) => { 45 | if (p.id === product.id) { 46 | p.quantity -= 1; 47 | } 48 | return p; 49 | }) 50 | .filter((p) => p.quantity > 0), 51 | }; 52 | }); 53 | }, 54 | clearCart: () => set({ products: [], items: 0, total: 0 }), 55 | })); 56 | 57 | export default useBasketStore; 58 | -------------------------------------------------------------------------------- /Components/SwipeableRow.tsx: -------------------------------------------------------------------------------- 1 | import { Ionicons } from '@expo/vector-icons'; 2 | import React, { Component, PropsWithChildren } from 'react'; 3 | import { Animated, StyleSheet, I18nManager, View } from 'react-native'; 4 | 5 | import { RectButton } from 'react-native-gesture-handler'; 6 | 7 | import Swipeable from 'react-native-gesture-handler/Swipeable'; 8 | 9 | export default class SwipeableRow extends Component void }>> { 10 | private renderRightActions = (_progress: Animated.AnimatedInterpolation, dragX: Animated.AnimatedInterpolation) => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | private swipeableRow?: Swipeable; 19 | 20 | private updateRef = (ref: Swipeable) => { 21 | this.swipeableRow = ref; 22 | }; 23 | private close = () => { 24 | this.swipeableRow?.close(); 25 | this.props.onDelete(); 26 | }; 27 | 28 | render() { 29 | const { children } = this.props; 30 | return ( 31 | 32 | {children} 33 | 34 | ); 35 | } 36 | } 37 | 38 | const styles = StyleSheet.create({ 39 | rightAction: { 40 | alignItems: 'center', 41 | flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row', 42 | backgroundColor: '#dd2c00', 43 | flex: 1, 44 | justifyContent: 'flex-end', 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deliverooclone", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "test": "jest --watchAll" 11 | }, 12 | "jest": { 13 | "preset": "jest-expo" 14 | }, 15 | "dependencies": { 16 | "@expo/vector-icons": "^13.0.0", 17 | "@gorhom/bottom-sheet": "^4.4.7", 18 | "@react-navigation/native": "^6.0.2", 19 | "deprecated-react-native-prop-types": "^4.2.1", 20 | "expo": "~49.0.7", 21 | "expo-font": "~11.4.0", 22 | "expo-haptics": "~12.4.0", 23 | "expo-linking": "~5.0.2", 24 | "expo-router": "2.0.0", 25 | "expo-splash-screen": "~0.20.5", 26 | "expo-status-bar": "~1.6.0", 27 | "expo-system-ui": "~2.4.0", 28 | "expo-web-browser": "~12.3.2", 29 | "react": "18.2.0", 30 | "react-dom": "18.2.0", 31 | "react-native": "0.72.3", 32 | "react-native-bouncy-checkbox": "^3.0.7", 33 | "react-native-confetti-cannon": "^1.5.2", 34 | "react-native-gesture-handler": "~2.12.0", 35 | "react-native-google-places-autocomplete": "^2.5.1", 36 | "react-native-maps": "1.7.1", 37 | "react-native-reanimated": "~3.3.0", 38 | "react-native-safe-area-context": "4.6.3", 39 | "react-native-screens": "~3.22.0", 40 | "react-native-web": "~0.19.6", 41 | "zustand": "^4.4.1" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.20.0", 45 | "@types/react": "~18.2.14", 46 | "jest": "^29.2.1", 47 | "jest-expo": "~49.0.0", 48 | "react-test-renderer": "18.2.0", 49 | "typescript": "^5.1.3" 50 | }, 51 | "overrides": { 52 | "react-refresh": "~0.14.0" 53 | }, 54 | "resolutions": { 55 | "react-refresh": "~0.14.0" 56 | }, 57 | "private": true 58 | } 59 | -------------------------------------------------------------------------------- /Components/Restaurants.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, ScrollView, StyleSheet, Image, TouchableOpacity } from 'react-native'; 2 | import React from 'react'; 3 | import { restaurants } from '@/assets/data/home'; 4 | import { Link } from 'expo-router'; 5 | import Colors from '../constants/Colors'; 6 | 7 | const Restaurants = () => { 8 | return ( 9 | 15 | {restaurants.map((restaurant, index) => ( 16 | 17 | 18 | 19 | 20 | 21 | {restaurant.name} 22 | 23 | {restaurant.rating} {restaurant.ratings} 24 | 25 | {restaurant.distance} 26 | 27 | 28 | 29 | 30 | ))} 31 | 32 | ); 33 | }; 34 | const styles = StyleSheet.create({ 35 | categoryCard: { 36 | width: 300, 37 | height: 250, 38 | backgroundColor: '#fff', 39 | marginEnd: 10, 40 | elevation: 2, 41 | shadowColor: '#000', 42 | shadowOffset: { 43 | width: 0, 44 | height: 4, 45 | }, 46 | shadowOpacity: 0.06, 47 | borderRadius: 4, 48 | }, 49 | categoryText: { 50 | paddingVertical: 5, 51 | fontSize: 14, 52 | fontWeight: 'bold', 53 | }, 54 | image: { 55 | flex: 5, 56 | width: undefined, 57 | height: undefined, 58 | }, 59 | categoryBox: { 60 | flex: 2, 61 | padding: 10, 62 | }, 63 | }); 64 | 65 | export default Restaurants; 66 | -------------------------------------------------------------------------------- /app/(modal)/location-search.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; 2 | import React, { useState } from 'react'; 3 | import MapView from 'react-native-maps'; 4 | import Colors from '@/constants/Colors'; 5 | import { useNavigation } from 'expo-router'; 6 | import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete'; 7 | import { Ionicons } from '@expo/vector-icons'; 8 | 9 | const LocationSearch = () => { 10 | const navigation = useNavigation(); 11 | const [location, setLocation] = useState({ 12 | latitude: 51.5078788, 13 | longitude: -0.0877321, 14 | latitudeDelta: 0.02, 15 | longitudeDelta: 0.02, 16 | }); 17 | 18 | return ( 19 | 20 | { 24 | const point = details?.geometry?.location; 25 | if (!point) return; 26 | setLocation({ 27 | ...location, 28 | latitude: point.lat, 29 | longitude: point.lng, 30 | }); 31 | }} 32 | query={{ 33 | key: process.env.EXPO_PUBLIC_GOOGLE_API_KEY, 34 | language: 'en', 35 | }} 36 | renderLeftButton={() => ( 37 | 38 | 39 | 40 | )} 41 | styles={{ 42 | container: { 43 | flex: 0, 44 | }, 45 | textInput: { 46 | backgroundColor: Colors.grey, 47 | paddingLeft: 35, 48 | borderRadius: 10, 49 | }, 50 | textInputContainer: { 51 | padding: 8, 52 | backgroundColor: '#fff', 53 | }, 54 | }} 55 | /> 56 | 57 | 58 | navigation.goBack()}> 59 | Confirm 60 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | const styles = StyleSheet.create({ 67 | map: { 68 | flex: 1, 69 | }, 70 | absoluteBox: { 71 | position: 'absolute', 72 | bottom: 20, 73 | width: '100%', 74 | }, 75 | button: { 76 | backgroundColor: Colors.primary, 77 | padding: 16, 78 | margin: 16, 79 | alignItems: 'center', 80 | borderRadius: 8, 81 | }, 82 | buttonText: { 83 | color: '#fff', 84 | fontWeight: 'bold', 85 | fontSize: 16, 86 | }, 87 | boxIcon: { 88 | position: 'absolute', 89 | left: 15, 90 | top: 18, 91 | zIndex: 1, 92 | }, 93 | }); 94 | 95 | export default LocationSearch; 96 | -------------------------------------------------------------------------------- /app/(modal)/dish.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, Image, TouchableOpacity } from 'react-native'; 2 | import React from 'react'; 3 | import { useLocalSearchParams, useRouter } from 'expo-router'; 4 | import { getDishById } from '@/assets/data/restaurant'; 5 | import Colors from '@/constants/Colors'; 6 | import { SafeAreaView } from 'react-native-safe-area-context'; 7 | import Animated, { FadeIn, FadeInDown, FadeInLeft, FadeInUp } from 'react-native-reanimated'; 8 | import * as Haptics from 'expo-haptics'; 9 | import useBasketStore from '@/store/basketStore'; 10 | 11 | const Dish = () => { 12 | const { id } = useLocalSearchParams(); 13 | const item = getDishById(+id)!; 14 | const router = useRouter(); 15 | const { addProduct } = useBasketStore(); 16 | 17 | const addToCart = () => { 18 | addProduct(item); 19 | Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); 20 | router.back(); 21 | }; 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | {item?.name} 29 | 30 | 31 | {item?.info} 32 | 33 | 34 | 35 | 36 | 37 | Add for ${item?.price} 38 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | const styles = StyleSheet.create({ 46 | container: { 47 | flex: 1, 48 | backgroundColor: '#fff', 49 | }, 50 | image: { 51 | width: '100%', 52 | height: 300, 53 | }, 54 | dishName: { 55 | fontSize: 24, 56 | fontWeight: 'bold', 57 | marginBottom: 8, 58 | }, 59 | dishInfo: { 60 | fontSize: 16, 61 | color: Colors.mediumDark, 62 | }, 63 | footer: { 64 | position: 'absolute', 65 | backgroundColor: '#fff', 66 | bottom: 0, 67 | left: 0, 68 | width: '100%', 69 | padding: 10, 70 | elevation: 10, 71 | shadowColor: '#000', 72 | shadowOffset: { width: 0, height: -10 }, 73 | shadowOpacity: 0.1, 74 | shadowRadius: 10, 75 | paddingTop: 20, 76 | }, 77 | fullButton: { 78 | backgroundColor: Colors.primary, 79 | padding: 16, 80 | borderRadius: 8, 81 | alignItems: 'center', 82 | }, 83 | footerText: { 84 | color: '#fff', 85 | fontWeight: 'bold', 86 | fontSize: 16, 87 | }, 88 | }); 89 | 90 | export default Dish; 91 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, useNavigation } from 'expo-router'; 2 | import CustomHeader from '@/Components/CustomHeader'; 3 | import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; 4 | import Colors from '../constants/Colors'; 5 | import { TouchableOpacity } from 'react-native'; 6 | import { Ionicons } from '@expo/vector-icons'; 7 | 8 | export const unstable_settings = { 9 | // Ensure that reloading on `/modal` keeps a back button present. 10 | initialRouteName: 'index', 11 | }; 12 | 13 | export default function RootLayoutNav() { 14 | const navigation = useNavigation(); 15 | 16 | return ( 17 | 18 | 19 | , 23 | }} 24 | /> 25 | ( 35 | { 37 | navigation.goBack(); 38 | }}> 39 | 40 | 41 | ), 42 | }} 43 | /> 44 | ( 50 | { 52 | navigation.goBack(); 53 | }}> 54 | 55 | 56 | ), 57 | }} 58 | /> 59 | ( 67 | { 70 | navigation.goBack(); 71 | }}> 72 | 73 | 74 | ), 75 | }} 76 | /> 77 | ( 82 | { 84 | navigation.goBack(); 85 | }}> 86 | 87 | 88 | ), 89 | }} 90 | /> 91 | 92 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /Components/CustomHeader.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, SafeAreaView, StyleSheet, TouchableOpacity, TextInput, Image } from 'react-native'; 2 | import React, { useRef } from 'react'; 3 | import { Ionicons } from '@expo/vector-icons'; 4 | import Colors from '../constants/Colors'; 5 | import { Link } from 'expo-router'; 6 | import BottomSheet from './BottomSheet'; 7 | import { BottomSheetModal } from '@gorhom/bottom-sheet'; 8 | 9 | const SearchBar = () => ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | const CustomHeader = () => { 26 | const bottomSheetRef = useRef(null); 27 | 28 | const openModal = () => { 29 | bottomSheetRef.current?.present(); 30 | }; 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Delivery · Now 43 | 44 | London 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | const styles = StyleSheet.create({ 59 | safeArea: { 60 | flex: 1, 61 | backgroundColor: '#fff', 62 | }, 63 | container: { 64 | height: 60, 65 | backgroundColor: '#fff', 66 | flexDirection: 'row', 67 | gap: 20, 68 | alignItems: 'center', 69 | justifyContent: 'space-between', 70 | paddingHorizontal: 20, 71 | }, 72 | bike: { 73 | width: 30, 74 | height: 30, 75 | }, 76 | titleContainer: { 77 | flex: 1, 78 | }, 79 | title: { 80 | fontSize: 14, 81 | color: Colors.medium, 82 | }, 83 | locationName: { 84 | flexDirection: 'row', 85 | alignItems: 'center', 86 | }, 87 | subtitle: { 88 | fontSize: 18, 89 | fontWeight: 'bold', 90 | }, 91 | profileButton: { 92 | backgroundColor: Colors.lightGrey, 93 | padding: 10, 94 | borderRadius: 50, 95 | }, 96 | searchContainer: { 97 | height: 60, 98 | backgroundColor: '#fff', 99 | }, 100 | searchSection: { 101 | flexDirection: 'row', 102 | gap: 10, 103 | flex: 1, 104 | paddingHorizontal: 20, 105 | alignItems: 'center', 106 | }, 107 | searchField: { 108 | flex: 1, 109 | backgroundColor: Colors.lightGrey, 110 | borderRadius: 8, 111 | flexDirection: 'row', 112 | alignItems: 'center', 113 | }, 114 | input: { 115 | padding: 10, 116 | color: Colors.mediumDark, 117 | }, 118 | searchIcon: { 119 | paddingLeft: 10, 120 | }, 121 | optionButton: { 122 | padding: 10, 123 | borderRadius: 50, 124 | }, 125 | }); 126 | 127 | export default CustomHeader; 128 | -------------------------------------------------------------------------------- /Components/BottomSheet.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, Button, TouchableOpacity, StyleSheet } from 'react-native'; 2 | import React, { forwardRef, useCallback, useMemo } from 'react'; 3 | import { BottomSheetBackdrop, BottomSheetModal, useBottomSheetModal } from '@gorhom/bottom-sheet'; 4 | import Colors from '../constants/Colors'; 5 | import { Link } from 'expo-router'; 6 | import { Ionicons } from '@expo/vector-icons'; 7 | 8 | export type Ref = BottomSheetModal; 9 | 10 | const BottomSheet = forwardRef((props, ref) => { 11 | const snapPoints = useMemo(() => ['50%'], []); 12 | const renderBackdrop = useCallback((props: any) => , []); 13 | const { dismiss } = useBottomSheetModal(); 14 | 15 | return ( 16 | 23 | 24 | 25 | 26 | Delivery 27 | 28 | 29 | Pickup 30 | 31 | 32 | 33 | Your Location 34 | 35 | 36 | 37 | 38 | Current location 39 | 40 | 41 | 42 | 43 | 44 | Arrival time 45 | 46 | 47 | 48 | Now 49 | 50 | 51 | 52 | 53 | dismiss()}> 54 | Confirm 55 | 56 | 57 | 58 | ); 59 | }); 60 | 61 | const styles = StyleSheet.create({ 62 | contentContainer: { 63 | flex: 1, 64 | }, 65 | toggle: { 66 | flexDirection: 'row', 67 | justifyContent: 'center', 68 | gap: 10, 69 | marginBottom: 32, 70 | }, 71 | toggleActive: { 72 | backgroundColor: Colors.primary, 73 | padding: 8, 74 | borderRadius: 32, 75 | paddingHorizontal: 30, 76 | }, 77 | activeText: { 78 | color: '#fff', 79 | fontWeight: '700', 80 | }, 81 | toggleInactive: { 82 | padding: 8, 83 | borderRadius: 32, 84 | paddingHorizontal: 30, 85 | }, 86 | inactiveText: { 87 | color: Colors.primary, 88 | }, 89 | button: { 90 | backgroundColor: Colors.primary, 91 | padding: 16, 92 | margin: 16, 93 | borderRadius: 4, 94 | alignItems: 'center', 95 | }, 96 | buttonText: { 97 | color: '#fff', 98 | fontWeight: 'bold', 99 | }, 100 | subheader: { 101 | fontSize: 16, 102 | fontWeight: '600', 103 | margin: 16, 104 | }, 105 | item: { 106 | flexDirection: 'row', 107 | gap: 8, 108 | alignItems: 'center', 109 | backgroundColor: '#fff', 110 | padding: 16, 111 | borderColor: Colors.grey, 112 | borderWidth: 1, 113 | }, 114 | }); 115 | 116 | export default BottomSheet; 117 | -------------------------------------------------------------------------------- /app/basket.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, FlatList, StyleSheet, TouchableOpacity } from 'react-native'; 2 | import React, { useState } from 'react'; 3 | import useBasketStore from '@/store/basketStore'; 4 | import Colors from '@/constants/Colors'; 5 | import { SafeAreaView } from 'react-native-safe-area-context'; 6 | import ConfettiCannon from 'react-native-confetti-cannon'; 7 | import { Link } from 'expo-router'; 8 | import SwipeableRow from '@/Components/SwipeableRow'; 9 | 10 | const Basket = () => { 11 | const { products, total, clearCart, reduceProduct } = useBasketStore(); 12 | const [order, setOrder] = useState(false); 13 | 14 | const FEES = { 15 | service: 2.99, 16 | delivery: 5.99, 17 | }; 18 | 19 | const startCheckout = () => { 20 | setOrder(true); 21 | clearCart(); 22 | }; 23 | 24 | return ( 25 | <> 26 | {order && } 27 | {order && ( 28 | 29 | Thank you for your order! 30 | 31 | 32 | New order 33 | 34 | 35 | 36 | )} 37 | {!order && ( 38 | <> 39 | Items} 42 | ItemSeparatorComponent={() => } 43 | renderItem={({ item }) => ( 44 | reduceProduct(item)}> 45 | 46 | {item.quantity}x 47 | {item.name} 48 | ${item.price * item.quantity} 49 | 50 | 51 | )} 52 | ListFooterComponent={ 53 | 54 | 55 | 56 | Subtotal 57 | ${total} 58 | 59 | 60 | 61 | Service fee 62 | ${FEES.service} 63 | 64 | 65 | 66 | Delivery fee 67 | ${FEES.delivery} 68 | 69 | 70 | 71 | Order Total 72 | ${(total + FEES.service + FEES.delivery).toFixed(2)} 73 | 74 | 75 | } 76 | /> 77 | 78 | 79 | 80 | 81 | Order now 82 | 83 | 84 | 85 | 86 | )} 87 | 88 | ); 89 | }; 90 | 91 | const styles = StyleSheet.create({ 92 | row: { 93 | flexDirection: 'row', 94 | backgroundColor: '#fff', 95 | padding: 10, 96 | gap: 20, 97 | alignItems: 'center', 98 | }, 99 | section: { 100 | fontSize: 20, 101 | fontWeight: 'bold', 102 | margin: 16, 103 | }, 104 | totalRow: { 105 | flexDirection: 'row', 106 | justifyContent: 'space-between', 107 | padding: 10, 108 | backgroundColor: '#fff', 109 | }, 110 | total: { 111 | fontSize: 18, 112 | color: Colors.medium, 113 | }, 114 | footer: { 115 | position: 'absolute', 116 | backgroundColor: '#fff', 117 | bottom: 0, 118 | left: 0, 119 | width: '100%', 120 | padding: 10, 121 | elevation: 10, 122 | shadowColor: '#000', 123 | shadowOffset: { width: 0, height: -10 }, 124 | shadowOpacity: 0.1, 125 | shadowRadius: 10, 126 | paddingTop: 20, 127 | }, 128 | fullButton: { 129 | backgroundColor: Colors.primary, 130 | paddingHorizontal: 16, 131 | borderRadius: 8, 132 | alignItems: 'center', 133 | justifyContent: 'center', 134 | flex: 1, 135 | height: 50, 136 | }, 137 | footerText: { 138 | color: '#fff', 139 | fontWeight: 'bold', 140 | fontSize: 16, 141 | }, 142 | orderBtn: { 143 | backgroundColor: Colors.primary, 144 | paddingHorizontal: 16, 145 | borderRadius: 8, 146 | alignItems: 'center', 147 | width: 250, 148 | height: 50, 149 | justifyContent: 'center', 150 | marginTop: 20, 151 | }, 152 | }); 153 | 154 | export default Basket; 155 | -------------------------------------------------------------------------------- /assets/data/restaurant.ts: -------------------------------------------------------------------------------- 1 | export const getDishById = (id: number) => { 2 | const meals = restaurant.food.flatMap((category) => category.meals); 3 | return meals.find((meal) => meal.id === id); 4 | }; 5 | export const restaurant = { 6 | name: 'Vapiano', 7 | rating: '4.5 Excellent', 8 | ratings: '(500+)', 9 | img: require('@/assets/data/r1.jpeg'), 10 | distance: '0.85 miles away', 11 | delivery: '10-20 min', 12 | tags: ['Italian', 'Pizza', 'Pasta', 'Salads', 'Vegetarian', 'Alcohol', 'Wine', 'Vegan Friendly'], 13 | about: 'The home of handmade fresh pasta, thin crust pizza, protein packed salads, homemade sauces and dressings too. Choose your pasta shape and add any extras you like.', 14 | food: [ 15 | { 16 | category: 'Meal Deals', 17 | meals: [ 18 | { 19 | id: 1, 20 | name: 'Pasta Power ✊', 21 | price: 17, 22 | info: 'Includes one garlic bread, one pasta and one soft drink.', 23 | img: require('@/assets/data/1.png'), 24 | }, 25 | { 26 | id: 2, 27 | name: 'Vegetariano 💚', 28 | price: 17, 29 | info: 'Includes one garlic bread, one vegetarian pasta and one soft drink', 30 | img: require('@/assets/data/2.png'), 31 | }, 32 | { 33 | id: 3, 34 | name: 'Vaps Date 💕', 35 | price: 40, 36 | info: 'Includes one garlic bread with or without cheese, choice of two pizzas, one bottle of wine or four bottles of Moretti', 37 | img: require('@/assets/data/3.png'), 38 | }, 39 | { 40 | id: 4, 41 | name: "Livin' your best life 😎", 42 | price: 80, 43 | info: 'Includes two garlic breads with or without cheese, four pizzas, two bottles of wine or eight bottles of beer or a mix of both', 44 | img: require('@/assets/data/4.png'), 45 | }, 46 | ], 47 | }, 48 | { 49 | category: 'Pasta', 50 | meals: [ 51 | { 52 | id: 5, 53 | name: 'Arrabbiata', 54 | price: 9.35, 55 | info: 'Tomato sauce, chilli, garlic, and onions', 56 | img: require('@/assets/data/5.png'), 57 | }, 58 | { 59 | id: 6, 60 | name: 'Pomodoro e Mozzarella', 61 | price: 10.75, 62 | info: 'Tomato sauce, onions, mozzarella', 63 | img: require('@/assets/data/6.png'), 64 | }, 65 | ], 66 | }, 67 | { 68 | category: 'Pizza', 69 | meals: [ 70 | { 71 | id: 7, 72 | name: 'Salame', 73 | price: 11.35, 74 | info: 'Spicy Italian sausage, tomato sauce, mozzarella', 75 | img: require('@/assets/data/7.png'), 76 | }, 77 | { 78 | id: 8, 79 | name: 'Margherity', 80 | price: 9.75, 81 | info: 'Tomato sauce, mozzarella', 82 | img: require('@/assets/data/8.png'), 83 | }, 84 | ], 85 | }, 86 | { 87 | category: 'Salad', 88 | meals: [ 89 | { 90 | id: 9, 91 | name: 'Insalata Mista Piccola', 92 | price: 5.99, 93 | info: 'Mixed leaf salad, cherry tomatoes and grated carrot. There can be no swaps, if you would like to add any extras please customise below.', 94 | img: require('@/assets/data/9.png'), 95 | }, 96 | { 97 | id: 10, 98 | name: 'Insalata Mista della Casa', 99 | price: 8.95, 100 | info: 'Large mixed salad. There can be no swaps, if you would like to add any extras please customise below.', 101 | img: require('@/assets/data/10.png'), 102 | }, 103 | ], 104 | }, 105 | { 106 | category: 'Meal Deals', 107 | meals: [ 108 | { 109 | id: 1, 110 | name: 'Pasta Power ✊', 111 | price: 17, 112 | info: 'Includes one garlic bread, one pasta and one soft drink.', 113 | img: require('@/assets/data/1.png'), 114 | }, 115 | { 116 | id: 2, 117 | name: 'Vegetariano 💚', 118 | price: 17, 119 | info: 'Includes one garlic bread, one vegetarian pasta and one soft drink', 120 | img: require('@/assets/data/2.png'), 121 | }, 122 | { 123 | id: 3, 124 | name: 'Vaps Date 💕', 125 | price: 40, 126 | info: 'Includes one garlic bread with or without cheese, choice of two pizzas, one bottle of wine or four bottles of Moretti', 127 | img: require('@/assets/data/3.png'), 128 | }, 129 | { 130 | id: 4, 131 | name: "Livin' your best life 😎", 132 | price: 80, 133 | info: 'Includes two garlic breads with or without cheese, four pizzas, two bottles of wine or eight bottles of beer or a mix of both', 134 | img: require('@/assets/data/4.png'), 135 | }, 136 | ], 137 | }, 138 | { 139 | category: 'Pasta', 140 | meals: [ 141 | { 142 | id: 5, 143 | name: 'Arrabbiata', 144 | price: 9.35, 145 | info: 'Tomato sauce, chilli, garlic, and onions', 146 | img: require('@/assets/data/5.png'), 147 | }, 148 | { 149 | id: 6, 150 | name: 'Pomodoro e Mozzarella', 151 | price: 10.75, 152 | info: 'Tomato sauce, onions, mozzarella', 153 | img: require('@/assets/data/6.png'), 154 | }, 155 | ], 156 | }, 157 | { 158 | category: 'Pizza', 159 | meals: [ 160 | { 161 | id: 7, 162 | name: 'Salame', 163 | price: 11.35, 164 | info: 'Spicy Italian sausage, tomato sauce, mozzarella', 165 | img: require('@/assets/data/7.png'), 166 | }, 167 | { 168 | id: 8, 169 | name: 'Margherity', 170 | price: 9.75, 171 | info: 'Tomato sauce, mozzarella', 172 | img: require('@/assets/data/8.png'), 173 | }, 174 | ], 175 | }, 176 | { 177 | category: 'Salad', 178 | meals: [ 179 | { 180 | id: 9, 181 | name: 'Insalata Mista Piccola', 182 | price: 5.99, 183 | info: 'Mixed leaf salad, cherry tomatoes and grated carrot. There can be no swaps, if you would like to add any extras please customise below.', 184 | img: require('@/assets/data/9.png'), 185 | }, 186 | { 187 | id: 10, 188 | name: 'Insalata Mista della Casa', 189 | price: 8.95, 190 | info: 'Large mixed salad. There can be no swaps, if you would like to add any extras please customise below.', 191 | img: require('@/assets/data/10.png'), 192 | }, 193 | ], 194 | }, 195 | ], 196 | }; 197 | -------------------------------------------------------------------------------- /app/(modal)/filter.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, Touchable, TouchableOpacity, FlatList, ListRenderItem, Button } from 'react-native'; 2 | import React, { useEffect, useState } from 'react'; 3 | import Colors from '../../constants/Colors'; 4 | import { useNavigation } from 'expo-router'; 5 | import categories from '@/assets/data/filter.json'; 6 | import { Ionicons } from '@expo/vector-icons'; 7 | import BouncyCheckbox from 'react-native-bouncy-checkbox'; 8 | import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; 9 | 10 | interface Category { 11 | name: string; 12 | count: number; 13 | checked?: boolean; 14 | } 15 | 16 | const ItemBox = () => ( 17 | <> 18 | 19 | 20 | 21 | Sort 22 | 23 | 24 | 25 | 26 | 27 | Hygiene rating 28 | 29 | 30 | 31 | 32 | 33 | Offers 34 | 35 | 36 | 37 | 38 | 39 | Dietary 40 | 41 | 42 | 43 | Categories 44 | 45 | ); 46 | 47 | const Filter = () => { 48 | const navigation = useNavigation(); 49 | const [items, setItems] = useState(categories); 50 | const [selected, setSelected] = useState([]); 51 | const flexWidth = useSharedValue(0); 52 | const scale = useSharedValue(0); 53 | 54 | useEffect(() => { 55 | const hasSelected = selected.length > 0; 56 | const selectedItems = items.filter((item) => item.checked); 57 | const newSelected = selectedItems.length > 0; 58 | 59 | if (hasSelected !== newSelected) { 60 | flexWidth.value = withTiming(newSelected ? 150 : 0); 61 | scale.value = withTiming(newSelected ? 1 : 0); 62 | } 63 | 64 | setSelected(selectedItems); 65 | }, [items]); 66 | 67 | const handleClearAll = () => { 68 | const updatedItems = items.map((item) => { 69 | item.checked = false; 70 | return item; 71 | }); 72 | setItems(updatedItems); 73 | }; 74 | 75 | const animatedStyles = useAnimatedStyle(() => { 76 | return { 77 | width: flexWidth.value, 78 | opacity: flexWidth.value > 0 ? 1 : 0, 79 | }; 80 | }); 81 | 82 | const animatedText = useAnimatedStyle(() => { 83 | return { 84 | transform: [{ scale: scale.value }], 85 | }; 86 | }); 87 | 88 | const renderItem: ListRenderItem = ({ item, index }) => ( 89 | 90 | 91 | {item.name} ({item.count}) 92 | 93 | { 101 | const isChecked = items[index].checked; 102 | 103 | const updatedItems = items.map((item) => { 104 | if (item.name === items[index].name) { 105 | item.checked = !isChecked; 106 | } 107 | 108 | return item; 109 | }); 110 | 111 | setItems(updatedItems); 112 | }} 113 | /> 114 | 115 | ); 116 | 117 | return ( 118 | 119 | } /> 120 | 121 | 122 | 123 | 124 | 125 | Clear all 126 | 127 | 128 | 129 | navigation.goBack()}> 130 | Done 131 | 132 | 133 | 134 | 135 | ); 136 | }; 137 | 138 | const styles = StyleSheet.create({ 139 | container: { 140 | flex: 1, 141 | padding: 24, 142 | backgroundColor: Colors.lightGrey, 143 | }, 144 | footer: { 145 | position: 'absolute', 146 | bottom: 0, 147 | left: 0, 148 | right: 0, 149 | height: 100, 150 | backgroundColor: '#fff', 151 | padding: 10, 152 | elevation: 10, 153 | shadowColor: '#000', 154 | shadowOpacity: 0.1, 155 | shadowRadius: 10, 156 | shadowOffset: { 157 | width: 0, 158 | height: -10, 159 | }, 160 | }, 161 | fullButton: { 162 | backgroundColor: Colors.primary, 163 | padding: 16, 164 | alignItems: 'center', 165 | borderRadius: 8, 166 | flex: 1, 167 | height: 56, 168 | }, 169 | footerText: { 170 | color: '#fff', 171 | fontWeight: 'bold', 172 | fontSize: 16, 173 | }, 174 | itemContainer: { 175 | backgroundColor: '#fff', 176 | padding: 8, 177 | borderRadius: 8, 178 | marginBottom: 16, 179 | }, 180 | header: { 181 | fontSize: 16, 182 | fontWeight: 'bold', 183 | marginBottom: 16, 184 | }, 185 | item: { 186 | flexDirection: 'row', 187 | gap: 20, 188 | alignItems: 'center', 189 | backgroundColor: '#fff', 190 | paddingVertical: 10, 191 | borderColor: Colors.grey, 192 | borderBottomWidth: 1, 193 | }, 194 | itemText: { 195 | flex: 1, 196 | }, 197 | row: { 198 | flexDirection: 'row', 199 | alignItems: 'center', 200 | padding: 10, 201 | backgroundColor: '#fff', 202 | }, 203 | btnContainer: { 204 | flexDirection: 'row', 205 | gap: 12, 206 | justifyContent: 'center', 207 | }, 208 | outlineButton: { 209 | borderColor: Colors.primary, 210 | borderWidth: 0.5, 211 | alignItems: 'center', 212 | justifyContent: 'center', 213 | borderRadius: 8, 214 | height: 56, 215 | }, 216 | outlineButtonText: { 217 | color: Colors.primary, 218 | fontWeight: 'bold', 219 | fontSize: 16, 220 | }, 221 | }); 222 | 223 | export default Filter; 224 | -------------------------------------------------------------------------------- /app/details.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, Image, TouchableOpacity, SectionList, ListRenderItem, ScrollView } from 'react-native'; 2 | import React, { useLayoutEffect, useRef, useState } from 'react'; 3 | import ParallaxScrollView from '@/Components/ParallaxScrollView'; 4 | import Colors from '@/constants/Colors'; 5 | import { restaurant } from '@/assets/data/restaurant'; 6 | import { Link, useNavigation } from 'expo-router'; 7 | import { Ionicons } from '@expo/vector-icons'; 8 | import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; 9 | import useBasketStore from '@/store/basketStore'; 10 | import { SafeAreaView } from 'react-native-safe-area-context'; 11 | 12 | const Details = () => { 13 | const navigation = useNavigation(); 14 | const [activeIndex, setActiveIndex] = useState(0); 15 | 16 | const opacity = useSharedValue(0); 17 | const animatedStyles = useAnimatedStyle(() => ({ 18 | opacity: opacity.value, 19 | })); 20 | 21 | const scrollRef = useRef(null); 22 | const itemsRef = useRef([]); 23 | 24 | const DATA = restaurant.food.map((item, index) => ({ 25 | title: item.category, 26 | data: item.meals, 27 | index, 28 | })); 29 | 30 | const { items, total } = useBasketStore(); 31 | 32 | useLayoutEffect(() => { 33 | navigation.setOptions({ 34 | headerTransparent: true, 35 | headerTitle: '', 36 | headerTintColor: Colors.primary, 37 | headerLeft: () => ( 38 | navigation.goBack()} style={styles.roundButton}> 39 | 40 | 41 | ), 42 | headerRight: () => ( 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ), 52 | }); 53 | }, []); 54 | 55 | const selectCategory = (index: number) => { 56 | const selected = itemsRef.current[index]; 57 | setActiveIndex(index); 58 | 59 | selected.measure((x) => { 60 | scrollRef.current?.scrollTo({ x: x - 16, y: 0, animated: true }); 61 | }); 62 | }; 63 | 64 | const onScroll = (event: any) => { 65 | const y = event.nativeEvent.contentOffset.y; 66 | if (y > 350) { 67 | opacity.value = withTiming(1); 68 | } else { 69 | opacity.value = withTiming(0); 70 | } 71 | }; 72 | 73 | const renderItem: ListRenderItem = ({ item, index }) => ( 74 | 75 | 76 | 77 | {item.name} 78 | {item.info} 79 | ${item.price} 80 | 81 | 82 | 83 | 84 | ); 85 | 86 | return ( 87 | <> 88 | } 95 | contentBackgroundColor={Colors.lightGrey} 96 | renderStickyHeader={() => ( 97 | 98 | {restaurant.name} 99 | 100 | )}> 101 | 102 | {restaurant.name} 103 | 104 | {restaurant.delivery} · {restaurant.tags.map((tag, index) => `${tag}${index < restaurant.tags.length - 1 ? ' · ' : ''}`)} 105 | 106 | {restaurant.about} 107 | `${item.id + index}`} 110 | scrollEnabled={false} 111 | sections={DATA} 112 | renderItem={renderItem} 113 | ItemSeparatorComponent={() => } 114 | SectionSeparatorComponent={() => } 115 | renderSectionHeader={({ section: { title, index } }) => {title}} 116 | /> 117 | 118 | 119 | 120 | {/* Sticky segments */} 121 | 122 | 123 | 124 | {restaurant.food.map((item, index) => ( 125 | (itemsRef.current[index] = ref!)} 127 | key={index} 128 | style={activeIndex === index ? styles.segmentButtonActive : styles.segmentButton} 129 | onPress={() => selectCategory(index)}> 130 | {item.category} 131 | 132 | ))} 133 | 134 | 135 | 136 | 137 | {/* Footer Basket */} 138 | {items > 0 && ( 139 | 140 | 141 | 142 | 143 | {items} 144 | View Basket 145 | ${total} 146 | 147 | 148 | 149 | 150 | )} 151 | 152 | ); 153 | }; 154 | 155 | const styles = StyleSheet.create({ 156 | detailsContainer: { 157 | backgroundColor: Colors.lightGrey, 158 | }, 159 | stickySection: { 160 | backgroundColor: '#fff', 161 | marginLeft: 70, 162 | height: 100, 163 | justifyContent: 'flex-end', 164 | }, 165 | roundButton: { 166 | width: 40, 167 | height: 40, 168 | borderRadius: 20, 169 | backgroundColor: '#fff', 170 | justifyContent: 'center', 171 | alignItems: 'center', 172 | }, 173 | bar: { 174 | flexDirection: 'row', 175 | alignItems: 'center', 176 | justifyContent: 'center', 177 | gap: 10, 178 | }, 179 | stickySectionText: { 180 | fontSize: 20, 181 | margin: 10, 182 | }, 183 | restaurantName: { 184 | fontSize: 30, 185 | margin: 16, 186 | }, 187 | restaurantDescription: { 188 | fontSize: 16, 189 | margin: 16, 190 | lineHeight: 22, 191 | color: Colors.medium, 192 | }, 193 | sectionHeader: { 194 | fontSize: 22, 195 | fontWeight: 'bold', 196 | marginTop: 40, 197 | margin: 16, 198 | }, 199 | item: { 200 | backgroundColor: '#fff', 201 | padding: 16, 202 | flexDirection: 'row', 203 | }, 204 | dishImage: { 205 | height: 80, 206 | width: 80, 207 | borderRadius: 4, 208 | }, 209 | dish: { 210 | fontSize: 16, 211 | fontWeight: 'bold', 212 | }, 213 | dishText: { 214 | fontSize: 14, 215 | color: Colors.mediumDark, 216 | paddingVertical: 4, 217 | }, 218 | stickySegments: { 219 | position: 'absolute', 220 | height: 50, 221 | left: 0, 222 | right: 0, 223 | top: 100, 224 | backgroundColor: '#fff', 225 | overflow: 'hidden', 226 | paddingBottom: 4, 227 | }, 228 | segmentsShadow: { 229 | backgroundColor: '#fff', 230 | justifyContent: 'center', 231 | shadowColor: '#000', 232 | shadowOffset: { 233 | width: 0, 234 | height: 4, 235 | }, 236 | shadowOpacity: 0.1, 237 | shadowRadius: 4, 238 | elevation: 5, 239 | width: '100%', 240 | height: '100%', 241 | }, 242 | segmentButton: { 243 | paddingHorizontal: 16, 244 | paddingVertical: 4, 245 | borderRadius: 50, 246 | }, 247 | segmentText: { 248 | color: Colors.primary, 249 | fontSize: 16, 250 | }, 251 | segmentButtonActive: { 252 | backgroundColor: Colors.primary, 253 | paddingHorizontal: 16, 254 | paddingVertical: 4, 255 | borderRadius: 50, 256 | }, 257 | segmentTextActive: { 258 | color: '#fff', 259 | fontWeight: 'bold', 260 | fontSize: 16, 261 | }, 262 | segmentScrollview: { 263 | paddingHorizontal: 16, 264 | alignItems: 'center', 265 | gap: 20, 266 | paddingBottom: 4, 267 | }, 268 | footer: { 269 | position: 'absolute', 270 | backgroundColor: '#fff', 271 | bottom: 0, 272 | left: 0, 273 | width: '100%', 274 | padding: 10, 275 | elevation: 10, 276 | shadowColor: '#000', 277 | shadowOffset: { width: 0, height: -10 }, 278 | shadowOpacity: 0.1, 279 | shadowRadius: 10, 280 | paddingTop: 20, 281 | }, 282 | fullButton: { 283 | backgroundColor: Colors.primary, 284 | paddingHorizontal: 16, 285 | borderRadius: 8, 286 | alignItems: 'center', 287 | flexDirection: 'row', 288 | flex: 1, 289 | justifyContent: 'space-between', 290 | height: 50, 291 | }, 292 | footerText: { 293 | color: '#fff', 294 | fontWeight: 'bold', 295 | fontSize: 16, 296 | }, 297 | basket: { 298 | color: '#fff', 299 | backgroundColor: '#19AA86', 300 | fontWeight: 'bold', 301 | padding: 8, 302 | borderRadius: 2, 303 | }, 304 | basketTotal: { 305 | color: '#fff', 306 | fontWeight: 'bold', 307 | fontSize: 16, 308 | }, 309 | }); 310 | 311 | export default Details; 312 | -------------------------------------------------------------------------------- /Components/ParallaxScrollView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Animated, Dimensions, View, StyleSheet } from 'react-native'; 3 | import { ViewPropTypes } from 'deprecated-react-native-prop-types'; 4 | 5 | import { bool, func, number, string } from 'prop-types'; 6 | 7 | const window = Dimensions.get('window'); 8 | 9 | const SCROLLVIEW_REF = 'ScrollView'; 10 | 11 | const pivotPoint = (a, b) => a - b; 12 | 13 | const renderEmpty = () => ; 14 | 15 | const noRender = () => ; 16 | 17 | // Override `toJSON` of interpolated value because of 18 | // an error when serializing style on view inside inspector. 19 | // See: https://github.com/jaysoo/react-native-parallax-scroll-view/issues/23 20 | const interpolate = (value, opts) => { 21 | const x = value.interpolate(opts); 22 | x.toJSON = () => x.__getValue(); 23 | return x; 24 | }; 25 | 26 | // Properties accepted by `ParallaxScrollView`. 27 | const IPropTypes = { 28 | backgroundColor: string, 29 | backgroundScrollSpeed: number, 30 | fadeOutForeground: bool, 31 | fadeOutBackground: bool, 32 | contentBackgroundColor: string, 33 | onChangeHeaderVisibility: func, 34 | parallaxHeaderHeight: number.isRequired, 35 | renderBackground: func, 36 | renderContentBackground: func, 37 | renderFixedHeader: func, 38 | renderForeground: func, 39 | renderScrollComponent: func, 40 | renderStickyHeader: func, 41 | stickyHeaderHeight: number, 42 | contentContainerStyle: ViewPropTypes.style, 43 | outputScaleValue: number, 44 | }; 45 | 46 | class ParallaxScrollView extends Component { 47 | constructor(props) { 48 | super(props); 49 | if (props.renderStickyHeader && !props.stickyHeaderHeight) { 50 | console.warn('Property `stickyHeaderHeight` must be set if `renderStickyHeader` is used.'); 51 | } 52 | if (props.renderParallaxHeader !== renderEmpty && !props.renderForeground) { 53 | console.warn('Property `renderParallaxHeader` is deprecated. Use `renderForeground` instead.'); 54 | } 55 | this.state = { 56 | scrollY: new Animated.Value(0), 57 | viewHeight: window.height, 58 | viewWidth: window.width, 59 | }; 60 | this.scrollY = new Animated.Value(0); 61 | this._footerComponent = { setNativeProps() {} }; // Initial stub 62 | this._footerHeight = 0; 63 | } 64 | 65 | animatedEvent = Animated.event([{ nativeEvent: { contentOffset: { y: this.scrollY } } }], { useNativeDriver: true }); 66 | 67 | render() { 68 | const { 69 | backgroundColor, 70 | backgroundScrollSpeed, 71 | children, 72 | contentBackgroundColor, 73 | fadeOutForeground, 74 | fadeOutBackground, 75 | parallaxHeaderHeight, 76 | renderBackground, 77 | renderContentBackground, 78 | renderFixedHeader, 79 | renderForeground, 80 | renderParallaxHeader, 81 | renderScrollComponent, 82 | renderStickyHeader, 83 | stickyHeaderHeight, 84 | style, 85 | contentContainerStyle, 86 | outputScaleValue, 87 | ...scrollViewProps 88 | } = this.props; 89 | 90 | const background = this._renderBackground({ 91 | fadeOutBackground, 92 | backgroundScrollSpeed, 93 | backgroundColor, 94 | parallaxHeaderHeight, 95 | stickyHeaderHeight, 96 | renderBackground, 97 | outputScaleValue, 98 | }); 99 | const foreground = this._renderForeground({ 100 | fadeOutForeground, 101 | parallaxHeaderHeight, 102 | stickyHeaderHeight, 103 | renderForeground: renderForeground || renderParallaxHeader, 104 | }); 105 | const bodyComponent = this._wrapChildren(children, { 106 | contentBackgroundColor, 107 | stickyHeaderHeight, 108 | renderContentBackground, 109 | contentContainerStyle, 110 | }); 111 | const footerSpacer = this._renderFooterSpacer({ contentBackgroundColor }); 112 | const maybeStickyHeader = this._maybeRenderStickyHeader({ 113 | parallaxHeaderHeight, 114 | stickyHeaderHeight, 115 | backgroundColor, 116 | renderFixedHeader, 117 | renderStickyHeader, 118 | }); 119 | const scrollElement = renderScrollComponent(scrollViewProps); 120 | return ( 121 | this._maybeUpdateViewDimensions(e)}> 122 | {background} 123 | {React.cloneElement( 124 | scrollElement, 125 | { 126 | ref: SCROLLVIEW_REF, 127 | style: [styles.scrollView, scrollElement.props.style], 128 | scrollEventThrottle: 1, 129 | // Using Native Driver greatly optimizes performance 130 | onScroll: Animated.event([{ nativeEvent: { contentOffset: { y: this.scrollY } } }], { useNativeDriver: true, listener: this._onScroll.bind(this) }), 131 | // onScroll: this._onScroll.bind(this) 132 | }, 133 | foreground, 134 | bodyComponent, 135 | footerSpacer 136 | )} 137 | {maybeStickyHeader} 138 | 139 | ); 140 | } 141 | 142 | /* 143 | * Expose `ScrollView` API so this component is composable with any component that expects a `ScrollView`. 144 | */ 145 | getScrollResponder() { 146 | return this.refs[SCROLLVIEW_REF]._component.getScrollResponder(); 147 | } 148 | getScrollableNode() { 149 | return this.getScrollResponder().getScrollableNode(); 150 | } 151 | getInnerViewNode() { 152 | return this.getScrollResponder().getInnerViewNode(); 153 | } 154 | scrollTo(...args) { 155 | this.getScrollResponder().scrollTo(...args); 156 | } 157 | setNativeProps(props) { 158 | this.refs[SCROLLVIEW_REF].setNativeProps(props); 159 | } 160 | 161 | /* 162 | * Private helpers 163 | */ 164 | 165 | _onScroll(e) { 166 | const { parallaxHeaderHeight, stickyHeaderHeight, onChangeHeaderVisibility, onScroll: prevOnScroll = () => {} } = this.props; 167 | this.props.scrollEvent && this.props.scrollEvent(e); 168 | const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); 169 | 170 | // This optimization wont run, since we update the animation value directly in onScroll event 171 | // this._maybeUpdateScrollPosition(e) 172 | 173 | if (e.nativeEvent.contentOffset.y >= p) { 174 | onChangeHeaderVisibility(false); 175 | } else { 176 | onChangeHeaderVisibility(true); 177 | } 178 | 179 | prevOnScroll(e); 180 | } 181 | 182 | // This optimizes the state update of current scrollY since we don't need to 183 | // perform any updates when user has scrolled past the pivot point. 184 | _maybeUpdateScrollPosition(e) { 185 | const { parallaxHeaderHeight, stickyHeaderHeight } = this.props; 186 | const { scrollY } = this; 187 | const { 188 | nativeEvent: { 189 | contentOffset: { y: offsetY }, 190 | }, 191 | } = e; 192 | const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); 193 | if (offsetY <= p || scrollY._value <= p) { 194 | scrollY.setValue(offsetY); 195 | } 196 | } 197 | 198 | _maybeUpdateViewDimensions(e) { 199 | const { 200 | nativeEvent: { 201 | layout: { width, height }, 202 | }, 203 | } = e; 204 | 205 | if (width !== this.state.viewWidth || height !== this.state.viewHeight) { 206 | this.setState({ 207 | viewWidth: width, 208 | viewHeight: height, 209 | }); 210 | } 211 | } 212 | 213 | _renderBackground({ fadeOutBackground, backgroundScrollSpeed, backgroundColor, parallaxHeaderHeight, stickyHeaderHeight, renderBackground, outputScaleValue }) { 214 | const { viewWidth, viewHeight } = this.state; 215 | const { scrollY } = this; 216 | const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); 217 | return ( 218 | 251 | {renderBackground()} 252 | 253 | ); 254 | } 255 | 256 | _renderForeground({ fadeOutForeground, parallaxHeaderHeight, stickyHeaderHeight, renderForeground }) { 257 | const { scrollY } = this; 258 | const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); 259 | return ( 260 | 261 | 275 | {renderForeground()} 276 | 277 | 278 | ); 279 | } 280 | 281 | _wrapChildren(children, { contentBackgroundColor, stickyHeaderHeight, contentContainerStyle, renderContentBackground }) { 282 | const { viewHeight } = this.state; 283 | const containerStyles = [{ backgroundColor: contentBackgroundColor }]; 284 | 285 | if (contentContainerStyle) containerStyles.push(contentContainerStyle); 286 | 287 | this.containerHeight = this.state.viewHeight; 288 | 289 | React.Children.forEach(children, (item) => { 290 | if (item && Object.keys(item).length != 0) { 291 | this.containerHeight = 0; 292 | } 293 | }); 294 | 295 | return ( 296 | { 299 | // Adjust the bottom height so we can scroll the parallax header all the way up. 300 | const { 301 | nativeEvent: { 302 | layout: { height }, 303 | }, 304 | } = e; 305 | const footerHeight = Math.max(0, viewHeight - height - stickyHeaderHeight); 306 | if (this._footerHeight !== footerHeight) { 307 | this._footerComponent.setNativeProps({ 308 | style: { height: footerHeight }, 309 | }); 310 | this._footerHeight = footerHeight; 311 | } 312 | }}> 313 | {renderContentBackground()} 314 | {children} 315 | 316 | ); 317 | } 318 | 319 | _renderFooterSpacer({ contentBackgroundColor }) { 320 | return ( 321 | { 323 | if (ref) { 324 | this._footerComponent = ref; 325 | } 326 | }} 327 | style={{ backgroundColor: contentBackgroundColor }} 328 | /> 329 | ); 330 | } 331 | 332 | _maybeRenderStickyHeader({ parallaxHeaderHeight, stickyHeaderHeight, backgroundColor, renderFixedHeader, renderStickyHeader }) { 333 | const { viewWidth } = this.state; 334 | const { scrollY } = this; 335 | if (renderStickyHeader || renderFixedHeader) { 336 | const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); 337 | return ( 338 | 346 | {renderStickyHeader ? ( 347 | 357 | 369 | {renderStickyHeader()} 370 | 371 | 372 | ) : null} 373 | {renderFixedHeader && renderFixedHeader()} 374 | 375 | ); 376 | } else { 377 | return null; 378 | } 379 | } 380 | } 381 | 382 | ParallaxScrollView.propTypes = IPropTypes; 383 | 384 | ParallaxScrollView.defaultProps = { 385 | backgroundScrollSpeed: 5, 386 | backgroundColor: '#000', 387 | contentBackgroundColor: '#fff', 388 | fadeOutForeground: true, 389 | onChangeHeaderVisibility: () => {}, 390 | renderScrollComponent: (props) => , 391 | renderBackground: renderEmpty, 392 | renderContentBackground: noRender, 393 | renderParallaxHeader: renderEmpty, // Deprecated (will be removed in 0.18.0) 394 | renderForeground: null, 395 | stickyHeaderHeight: 0, 396 | contentContainerStyle: null, 397 | outputScaleValue: 5, 398 | }; 399 | 400 | const styles = StyleSheet.create({ 401 | container: { 402 | flex: 1, 403 | backgroundColor: 'transparent', 404 | }, 405 | parallaxHeaderContainer: { 406 | backgroundColor: 'transparent', 407 | overflow: 'hidden', 408 | }, 409 | parallaxHeader: { 410 | backgroundColor: 'transparent', 411 | overflow: 'hidden', 412 | }, 413 | backgroundImage: { 414 | position: 'absolute', 415 | backgroundColor: 'transparent', 416 | overflow: 'hidden', 417 | top: 0, 418 | }, 419 | stickyHeader: { 420 | backgroundColor: 'transparent', 421 | position: 'absolute', 422 | overflow: 'hidden', 423 | top: 0, 424 | left: 0, 425 | }, 426 | scrollView: { 427 | backgroundColor: 'transparent', 428 | }, 429 | }); 430 | 431 | export default ParallaxScrollView; 432 | --------------------------------------------------------------------------------