├── .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 |
--------------------------------------------------------------------------------
]