├── src ├── @types │ ├── png.d.ts │ └── svg.d.ts ├── assets │ ├── corn.png │ ├── board.png │ ├── empty.png │ ├── fries.png │ ├── leaves.png │ ├── tomato.png │ ├── greek-pizza.png │ ├── marshmelow.png │ ├── itallian-pizza.png │ ├── piece-tomato.png │ ├── veggie-pizza.png │ ├── bottom.svg │ ├── arrow-left.svg │ ├── heart.svg │ └── cart.svg ├── global │ └── styles │ │ ├── styles.d.ts │ │ └── theme.ts └── screens │ └── Main │ ├── components │ ├── Content │ │ ├── components │ │ │ ├── Additionals │ │ │ │ ├── styles.ts │ │ │ │ └── index.tsx │ │ │ └── AddButtons │ │ │ │ ├── styles.ts │ │ │ │ └── index.tsx │ │ ├── styles.ts │ │ └── index.tsx │ ├── OptionsSlider │ │ ├── utils │ │ │ └── index.ts │ │ ├── styles.ts │ │ └── index.tsx │ └── Header │ │ ├── styles.ts │ │ └── index.tsx │ ├── styles.ts │ ├── utils │ └── pizzas.ts │ └── index.tsx ├── assets ├── demo.gif ├── icon.png ├── favicon.png ├── splash.png └── adaptive-icon.png ├── tsconfig.json ├── .expo-shared └── assets.json ├── .gitignore ├── babel.config.js ├── README.md ├── metro.config.js ├── App.tsx ├── app.json └── package.json /src/@types/png.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png"; 2 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/assets/splash.png -------------------------------------------------------------------------------- /src/assets/corn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/src/assets/corn.png -------------------------------------------------------------------------------- /src/assets/board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/src/assets/board.png -------------------------------------------------------------------------------- /src/assets/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/src/assets/empty.png -------------------------------------------------------------------------------- /src/assets/fries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/src/assets/fries.png -------------------------------------------------------------------------------- /src/assets/leaves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/src/assets/leaves.png -------------------------------------------------------------------------------- /src/assets/tomato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/src/assets/tomato.png -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /src/assets/greek-pizza.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/src/assets/greek-pizza.png -------------------------------------------------------------------------------- /src/assets/marshmelow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/src/assets/marshmelow.png -------------------------------------------------------------------------------- /src/assets/itallian-pizza.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/src/assets/itallian-pizza.png -------------------------------------------------------------------------------- /src/assets/piece-tomato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/src/assets/piece-tomato.png -------------------------------------------------------------------------------- /src/assets/veggie-pizza.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lklima/motion-pizza/HEAD/src/assets/veggie-pizza.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: ["react-native-reanimated/plugin"], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/@types/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | import React from "react"; 3 | import { SvgProps } from "react-native-svg"; 4 | const content: React.FC; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /src/global/styles/styles.d.ts: -------------------------------------------------------------------------------- 1 | import "styled-components"; 2 | import theme from "./theme"; 3 | 4 | declare module "styled-components" { 5 | type ThemeType = typeof theme; 6 | 7 | export interface DefaultTheme extends ThemeType {} 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![FotoJet](./assets/demo.gif) 2 | 3 | # MOTION-PIZZA 4 | 5 | React Native animated app with reanimated + expo. 6 | 7 | Give me a ⭐️ if liked it 8 | 9 | Concept Design: https://dribbble.com/shots/15694619-Pizza-Food-Delivery-App 10 | -------------------------------------------------------------------------------- /src/assets/bottom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/global/styles/theme.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | colors: { 3 | white: "#FFFFFF", 4 | gray: "#E8E9EB", 5 | black: "#190e05", 6 | yellow: "#FFD35A", 7 | orange: "#B44C1C", 8 | brown: "#361413", 9 | text: "#848484", 10 | }, 11 | fonts: { 12 | light: "SourceSerifPro_300Light", 13 | regular: "SourceSerifPro_400Regular", 14 | bold: "SourceSerifPro_600SemiBold", 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/screens/Main/components/Content/components/Additionals/styles.ts: -------------------------------------------------------------------------------- 1 | import Animated from "react-native-reanimated"; 2 | import styled from "styled-components/native"; 3 | 4 | export const Container = styled.View` 5 | position: absolute; 6 | width: 150px; 7 | height: 180px; 8 | justify-content: center; 9 | z-index: 9999; 10 | `; 11 | 12 | export const Pic = styled(Animated.Image)` 13 | height: 60px; 14 | width: 60px; 15 | position: absolute; 16 | `; 17 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require("expo/metro-config"); 2 | 3 | module.exports = (() => { 4 | const config = getDefaultConfig(__dirname); 5 | 6 | const { transformer, resolver } = config; 7 | 8 | config.transformer = { 9 | ...transformer, 10 | babelTransformerPath: require.resolve("react-native-svg-transformer"), 11 | }; 12 | config.resolver = { 13 | ...resolver, 14 | assetExts: resolver.assetExts.filter((ext) => ext !== "svg"), 15 | sourceExts: [...resolver.sourceExts, "svg"], 16 | }; 17 | 18 | return config; 19 | })(); 20 | -------------------------------------------------------------------------------- /src/screens/Main/components/OptionsSlider/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const data = [ 2 | { name: "fries", image: require("../../../../../assets/fries.png"), price: 0.5 }, 3 | { name: "tomato", image: require("../../../../../assets/tomato.png"), price: 0.5 }, 4 | { name: "corn", image: require("../../../../../assets/corn.png"), price: 0.5 }, 5 | { 6 | name: "marshmaloww", 7 | image: require("../../../../../assets/marshmelow.png"), 8 | price: 0.5, 9 | }, 10 | { name: "fries", image: require("../../../../../assets/fries.png"), price: 0.5 }, 11 | { 12 | name: "marshmaloww", 13 | image: require("../../../../../assets/marshmelow.png"), 14 | price: 0.5, 15 | }, 16 | ]; 17 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | SourceSerifPro_400Regular, 3 | SourceSerifPro_600SemiBold, 4 | SourceSerifPro_300Light, 5 | useFonts, 6 | } from "@expo-google-fonts/source-serif-pro"; 7 | import { ThemeProvider } from "styled-components"; 8 | 9 | import Main from "./src/screens/Main"; 10 | 11 | import theme from "./src/global/styles/theme"; 12 | 13 | export default function App() { 14 | const [isFontLoaded] = useFonts({ 15 | SourceSerifPro_400Regular, 16 | SourceSerifPro_600SemiBold, 17 | SourceSerifPro_300Light, 18 | }); 19 | 20 | if (!isFontLoaded) { 21 | return <>; 22 | } 23 | 24 | return ( 25 | 26 |
27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/screens/Main/components/OptionsSlider/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components/native"; 2 | 3 | export const Content = styled.View` 4 | width: 100%; 5 | align-items: center; 6 | margin-top: 30px; 7 | `; 8 | 9 | export const OptionsCount = styled.Text` 10 | font-size: 18px; 11 | font-family: ${({ theme }) => theme.fonts.light}; 12 | color: ${({ theme }) => theme.colors.text}; 13 | `; 14 | 15 | export const OptionsButton = styled.TouchableOpacity.attrs({ 16 | activeOpacity: 0.7, 17 | })` 18 | opacity: ${({ disabled }) => (disabled ? 0.4 : 1)}; 19 | `; 20 | 21 | export const OptionsImage = styled.Image.attrs({ 22 | resizeMode: "contain", 23 | })` 24 | height: 90px; 25 | width: 150px; 26 | `; 27 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "motion-pizza", 4 | "slug": "motion-pizza", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/adaptive-icon.png", 26 | "backgroundColor": "#FFFFFF" 27 | } 28 | }, 29 | "web": { 30 | "favicon": "./assets/favicon.png" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/screens/Main/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import Animated from "react-native-reanimated"; 2 | import styled from "styled-components/native"; 3 | 4 | export const Header = styled.View` 5 | width: 100%; 6 | flex-direction: row; 7 | justify-content: space-between; 8 | align-items: center; 9 | padding: 0 25px; 10 | `; 11 | 12 | export const TextArea = styled.View` 13 | flex: 1; 14 | align-items: center; 15 | height: 75px; 16 | `; 17 | 18 | export const Title = styled(Animated.Text)` 19 | font-size: 40px; 20 | font-family: ${({ theme }) => theme.fonts.bold}; 21 | color: ${({ theme }) => theme.colors.black}; 22 | text-align: center; 23 | `; 24 | 25 | export const SubTitle = styled(Animated.Text)` 26 | font-size: 18px; 27 | font-family: ${({ theme }) => theme.fonts.regular}; 28 | color: ${({ theme }) => theme.colors.text}; 29 | text-align: center; 30 | `; 31 | -------------------------------------------------------------------------------- /src/screens/Main/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components/native"; 2 | import Bottom from "../../assets/bottom.svg"; 3 | 4 | export const Container = styled.SafeAreaView` 5 | flex: 1; 6 | background: white; 7 | align-items: center; 8 | `; 9 | 10 | export const AddButton = styled.TouchableOpacity.attrs({ 11 | activeOpacity: 0.7, 12 | })` 13 | height: 50px; 14 | width: 180px; 15 | border-radius: 15px; 16 | background: ${({ theme }) => theme.colors.brown}; 17 | position: absolute; 18 | align-items: center; 19 | justify-content: center; 20 | bottom: 5.2%; 21 | z-index: 10; 22 | flex-direction: row; 23 | `; 24 | 25 | export const AddButtonText = styled.Text` 26 | color: ${({ theme }) => theme.colors.white}; 27 | font-family: ${({ theme }) => theme.fonts.bold}; 28 | font-size: 20px; 29 | margin-left: 15px; 30 | `; 31 | 32 | export const BottomBar = styled(Bottom)` 33 | position: absolute; 34 | bottom: -5%; 35 | `; 36 | -------------------------------------------------------------------------------- /src/screens/Main/components/Content/components/AddButtons/styles.ts: -------------------------------------------------------------------------------- 1 | import Animated from "react-native-reanimated"; 2 | import styled from "styled-components/native"; 3 | 4 | export const AdddButton = styled(Animated.View)` 5 | height: 50px; 6 | width: 50px; 7 | border-radius: 50px; 8 | background-color: ${({ theme }) => theme.colors.white}; 9 | box-shadow: 2px 2px 20px ${({ theme }) => theme.colors.gray}; 10 | elevation: 10; 11 | align-items: center; 12 | justify-content: center; 13 | position: absolute; 14 | left: 45px; 15 | z-index: 999; 16 | `; 17 | 18 | export const RemoveButton = styled(Animated.View)` 19 | height: 50px; 20 | width: 50px; 21 | border-radius: 50px; 22 | background-color: ${({ theme }) => theme.colors.white}; 23 | box-shadow: 2px 2px 20px ${({ theme }) => theme.colors.gray}; 24 | align-items: center; 25 | justify-content: center; 26 | position: absolute; 27 | right: 45px; 28 | z-index: 999; 29 | elevation: 10; 30 | `; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "motion-pizza", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "eject": "expo eject" 11 | }, 12 | "dependencies": { 13 | "@expo-google-fonts/source-serif-pro": "^0.2.2", 14 | "expo": "^45.0.0", 15 | "expo-font": "~10.1.0", 16 | "expo-status-bar": "~1.3.0", 17 | "react": "17.0.2", 18 | "react-native": "0.68.2", 19 | "react-native-reanimated": "~2.8.0", 20 | "react-native-svg": "12.3.0", 21 | "styled-components": "^5.3.5", 22 | "use-count-up": "^3.0.1" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.12.9", 26 | "@types/react": "~17.0.21", 27 | "@types/react-native": "~0.67.6", 28 | "@types/styled-components-react-native": "^5.1.3", 29 | "react-native-svg-transformer": "^1.0.0", 30 | "typescript": "~4.3.5" 31 | }, 32 | "private": true 33 | } 34 | -------------------------------------------------------------------------------- /src/screens/Main/components/Content/components/AddButtons/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Feather } from "@expo/vector-icons"; 3 | import { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated"; 4 | 5 | import * as S from "./styles"; 6 | 7 | interface Props { 8 | index: number; 9 | } 10 | 11 | export default function AddButtons({ index }: Props) { 12 | const opacity = useSharedValue(0); 13 | 14 | const buttonAnimatedStyle = useAnimatedStyle(() => ({ 15 | opacity: opacity.value, 16 | })); 17 | 18 | useEffect(() => { 19 | if (index === 0) { 20 | opacity.value = withTiming(0); 21 | } else { 22 | opacity.value = withTiming(1); 23 | } 24 | }, [index]); 25 | 26 | return ( 27 | <> 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/screens/Main/utils/pizzas.ts: -------------------------------------------------------------------------------- 1 | import { ImageSourcePropType } from "react-native"; 2 | 3 | interface PricesProps { 4 | [key: string]: number; 5 | } 6 | 7 | interface PizzaProps { 8 | name: string; 9 | desc: string; 10 | prices: PricesProps; 11 | pic: ImageSourcePropType; 12 | } 13 | 14 | export const pizzas: Array = [ 15 | { 16 | name: "Pizza", 17 | desc: "Pick your favourite", 18 | prices: { s: 0, m: 0, l: 0 }, 19 | pic: require("../../../assets/empty.png"), 20 | }, 21 | { 22 | name: "Italian", 23 | desc: "tomato sauce & mozzarella", 24 | prices: { s: 5.5, m: 7.5, l: 9.5 }, 25 | pic: require("../../../assets/itallian-pizza.png"), 26 | }, 27 | { 28 | name: "Veggie", 29 | desc: "fresh veggies & cheese", 30 | prices: { s: 6, m: 8, l: 10 }, 31 | pic: require("../../../assets/veggie-pizza.png"), 32 | }, 33 | { 34 | name: "Greek", 35 | desc: "spicy pizza with mozzarella", 36 | prices: { s: 7.5, m: 9.5, l: 11.5 }, 37 | pic: require("../../../assets/greek-pizza.png"), 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /src/screens/Main/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useWindowDimensions } from "react-native"; 3 | import { StatusBar } from "expo-status-bar"; 4 | 5 | import * as S from "./styles"; 6 | 7 | import Header from "./components/Header"; 8 | import Content from "./components/Content"; 9 | import OptionsSlider from "./components/OptionsSlider"; 10 | 11 | import CartIcon from "../../assets/cart.svg"; 12 | 13 | export default function Main() { 14 | const { width } = useWindowDimensions(); 15 | 16 | const [currentIndex, setCurrentIndex] = useState(0); 17 | const [aditionals, setAditionals] = useState([]); 18 | 19 | return ( 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | Add to cart 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/assets/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/screens/Main/components/OptionsSlider/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FlatList, ImageSourcePropType } from "react-native"; 3 | 4 | import * as S from "./styles"; 5 | 6 | import { data } from "./utils"; 7 | 8 | export interface Additional { 9 | name: string; 10 | image: ImageSourcePropType; 11 | price: number; 12 | } 13 | 14 | interface Props { 15 | aditionals: Additional[]; 16 | setAditionals(item: object): void; 17 | } 18 | 19 | export default function OptionsSlider({ aditionals, setAditionals }: Props) { 20 | function add(additional: Additional) { 21 | if (aditionals.length < 3) { 22 | setAditionals((previous: Additional[]) => [...previous, additional]); 23 | } 24 | } 25 | 26 | return ( 27 | 28 | {aditionals.length}/3 29 | String(index)} 32 | horizontal 33 | showsHorizontalScrollIndicator={false} 34 | disableIntervalMomentum 35 | decelerationRate="fast" 36 | contentContainerStyle={{ marginLeft: -20 }} 37 | style={{ zIndex: 999 }} 38 | renderItem={({ item }) => { 39 | const hasAdded = aditionals.includes(item); 40 | 41 | return ( 42 | add(item)}> 43 | 44 | 45 | ); 46 | }} 47 | /> 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/assets/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/screens/Main/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { FlatList } from "react-native"; 3 | import { 4 | useAnimatedStyle, 5 | useSharedValue, 6 | withDelay, 7 | withTiming, 8 | } from "react-native-reanimated"; 9 | 10 | import * as S from "./styles"; 11 | 12 | import ArrowLeft from "../../../../assets/arrow-left.svg"; 13 | import Heart from "../../../../assets/heart.svg"; 14 | 15 | import { pizzas } from "../../utils/pizzas"; 16 | 17 | interface Props { 18 | index: number; 19 | } 20 | 21 | export default function Header({ index }: Props) { 22 | const listRef = useRef(null); 23 | 24 | const opacity = useSharedValue(1); 25 | 26 | useEffect(() => { 27 | if (index !== 0) { 28 | opacity.value = withTiming(0.1); 29 | } 30 | 31 | setTimeout(() => { 32 | listRef.current?.scrollToIndex({ animated: true, index }); 33 | }, 300); 34 | 35 | opacity.value = withDelay(600, withTiming(1)); 36 | }, [index]); 37 | 38 | const textAniamtedStyle = useAnimatedStyle(() => ({ 39 | opacity: opacity.value, 40 | })); 41 | 42 | return ( 43 | 44 | 45 | 46 | pizza.name} 50 | showsVerticalScrollIndicator={false} 51 | scrollEnabled={false} 52 | renderItem={({ item }) => ( 53 | <> 54 | {item.name} 55 | {item.desc} 56 | 57 | )} 58 | /> 59 | 60 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/assets/cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/screens/Main/components/Content/styles.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from "react-native"; 2 | import Animated from "react-native-reanimated"; 3 | import styled from "styled-components/native"; 4 | 5 | const { width } = Dimensions.get("window"); 6 | 7 | interface OptionsTextProps { 8 | isSelected: boolean; 9 | } 10 | 11 | export const Container = styled.View` 12 | width: 100%; 13 | align-items: center; 14 | margin-top: 10px; 15 | `; 16 | 17 | export const ImageContent = styled.View` 18 | width: 100%; 19 | align-items: center; 20 | justify-content: center; 21 | `; 22 | 23 | export const RoundBoardContent = styled(Animated.View)` 24 | position: absolute; 25 | shadow-opacity: 0.6; 26 | shadow-radius: 25px; 27 | shadow-color: ${({ theme }) => theme.colors.brown}; 28 | shadow-offset: 0px 20px; 29 | `; 30 | 31 | export const RoundBoard = styled.Image``; 32 | 33 | export const Leaves = styled(Animated.Image)``; 34 | 35 | export const PizzaContent = styled.View` 36 | height: 300px; 37 | width: ${width}px; 38 | align-items: center; 39 | justify-content: center; 40 | `; 41 | 42 | export const Pizza = styled(Animated.Image)` 43 | height: 250px; 44 | width: 250px; 45 | `; 46 | 47 | export const Price = styled.Text` 48 | font-size: 40px; 49 | font-family: ${({ theme }) => theme.fonts.bold}; 50 | color: ${({ theme }) => theme.colors.black}; 51 | margin-top: 10px; 52 | z-index: 20; 53 | `; 54 | 55 | export const Options = styled.View` 56 | flex-direction: row; 57 | box-shadow: 2px 2px 15px ${({ theme }) => theme.colors.gray}; 58 | background: white; 59 | margin-top: 30px; 60 | height: 55px; 61 | width: 200px; 62 | border-radius: 30px; 63 | align-items: center; 64 | justify-content: space-between; 65 | elevation: 10; 66 | `; 67 | 68 | export const OptionsButtonText = styled.TouchableOpacity.attrs({ activeOpacity: 1 })` 69 | padding: 0px 20px; 70 | z-index: 11; 71 | `; 72 | 73 | export const OptionsText = styled.Text` 74 | font-size: 25px; 75 | font-family: ${({ theme }) => theme.fonts.regular}; 76 | color: ${({ theme, isSelected }) => 77 | isSelected ? theme.colors.black : theme.colors.text}; 78 | padding-top: 5px; 79 | `; 80 | 81 | export const OptionsSlider = styled(Animated.View)` 82 | height: 55px; 83 | width: 55px; 84 | border-radius: 50px; 85 | background-color: ${({ theme }) => theme.colors.yellow}; 86 | box-shadow: 2px 2px 20px ${({ theme }) => theme.colors.yellow}; 87 | align-items: center; 88 | justify-content: center; 89 | position: absolute; 90 | `; 91 | 92 | export const AdddButton = styled.TouchableOpacity.attrs({ activeOpacity: 0.7 })` 93 | height: 50px; 94 | width: 50px; 95 | border-radius: 50px; 96 | background-color: ${({ theme }) => theme.colors.white}; 97 | box-shadow: 2px 2px 20px ${({ theme }) => theme.colors.gray}; 98 | align-items: center; 99 | justify-content: center; 100 | position: absolute; 101 | left: 45px; 102 | z-index: 20; 103 | `; 104 | 105 | export const RemoveButton = styled.TouchableOpacity.attrs({ activeOpacity: 0.7 })` 106 | height: 50px; 107 | width: 50px; 108 | border-radius: 50px; 109 | background-color: ${({ theme }) => theme.colors.white}; 110 | box-shadow: 2px 2px 20px ${({ theme }) => theme.colors.gray}; 111 | align-items: center; 112 | justify-content: center; 113 | position: absolute; 114 | right: 45px; 115 | z-index: 20; 116 | `; 117 | -------------------------------------------------------------------------------- /src/screens/Main/components/Content/components/Additionals/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { 3 | useAnimatedStyle, 4 | useSharedValue, 5 | withDelay, 6 | withTiming, 7 | } from "react-native-reanimated"; 8 | 9 | import { Additional } from "../../../OptionsSlider"; 10 | 11 | import * as S from "./styles"; 12 | 13 | import tomato from "../../../../../../assets/piece-tomato.png"; 14 | 15 | interface Props { 16 | aditionals: Additional[]; 17 | } 18 | 19 | export default function Additionals({ aditionals }: Props) { 20 | const add1Opacity = useSharedValue(0); 21 | const add1Scale = useSharedValue(4); 22 | const add1TranslateY = useSharedValue(-50); 23 | 24 | const add2Opacity = useSharedValue(0); 25 | const add2Scale = useSharedValue(3); 26 | const add2TranslateY = useSharedValue(-80); 27 | 28 | const add3Opacity = useSharedValue(0); 29 | const add3Scale = useSharedValue(8); 30 | 31 | const add4Opacity = useSharedValue(0); 32 | const add4Scale = useSharedValue(3); 33 | const add4TranslateY = useSharedValue(50); 34 | 35 | const add5Opacity = useSharedValue(0); 36 | const add5Scale = useSharedValue(3); 37 | const add5TranslateY = useSharedValue(50); 38 | 39 | const image1AnimatedStyle = useAnimatedStyle(() => ({ 40 | opacity: add1Opacity.value, 41 | transform: [{ scale: add1Scale.value }, { translateY: add1TranslateY.value }], 42 | })); 43 | const image2AnimatedStyle = useAnimatedStyle(() => ({ 44 | opacity: add2Opacity.value, 45 | transform: [{ scale: add2Scale.value }, { translateY: add2TranslateY.value }], 46 | })); 47 | const image3AnimatedStyle = useAnimatedStyle(() => ({ 48 | opacity: add3Opacity.value, 49 | transform: [{ scale: add3Scale.value }], 50 | })); 51 | const image4AnimatedStyle = useAnimatedStyle(() => ({ 52 | opacity: add4Opacity.value, 53 | transform: [{ scale: add4Scale.value }, { translateY: add4TranslateY.value }], 54 | })); 55 | const image5AnimatedStyle = useAnimatedStyle(() => ({ 56 | opacity: add5Opacity.value, 57 | transform: [{ scale: add5Scale.value }, { translateY: add5TranslateY.value }], 58 | })); 59 | 60 | useEffect(() => { 61 | if (aditionals.length > 0) { 62 | add1Opacity.value = withTiming(1); 63 | add1Scale.value = withTiming(1); 64 | add1TranslateY.value = withTiming(0); 65 | 66 | add2Opacity.value = withDelay(300, withTiming(1)); 67 | add2Scale.value = withDelay(300, withTiming(1)); 68 | add2TranslateY.value = withDelay(300, withTiming(0)); 69 | 70 | add3Opacity.value = withDelay(600, withTiming(1)); 71 | add3Scale.value = withDelay(600, withTiming(1)); 72 | 73 | add4Opacity.value = withDelay(900, withTiming(1)); 74 | add4Scale.value = withDelay(900, withTiming(1)); 75 | add4TranslateY.value = withDelay(900, withTiming(0)); 76 | 77 | add5Opacity.value = withDelay(1200, withTiming(1)); 78 | add5Scale.value = withDelay(1200, withTiming(1)); 79 | add5TranslateY.value = withDelay(1200, withTiming(0)); 80 | } 81 | }, [aditionals]); 82 | 83 | return ( 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/screens/Main/components/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { 3 | useAnimatedStyle, 4 | useSharedValue, 5 | withTiming, 6 | withDelay, 7 | } from "react-native-reanimated"; 8 | import { 9 | FlatList, 10 | NativeScrollEvent, 11 | NativeSyntheticEvent, 12 | ViewToken, 13 | } from "react-native"; 14 | import { CountUp } from "use-count-up"; 15 | 16 | import * as S from "./styles"; 17 | 18 | import board from "../../../../assets/board.png"; 19 | import leaves from "../../../../assets/leaves.png"; 20 | 21 | import { pizzas } from "../../utils/pizzas"; 22 | 23 | import { Additional } from "../OptionsSlider"; 24 | import AddButtons from "./components/AddButtons"; 25 | import Additionals from "./components/Additionals"; 26 | 27 | interface ViableItems { 28 | viewableItems: ViewToken[]; 29 | } 30 | 31 | interface Props { 32 | index: number; 33 | aditionals: Additional[]; 34 | setIndex(index: number): void; 35 | } 36 | 37 | export default function Content({ index, aditionals, setIndex }: Props) { 38 | const left = useSharedValue(71); 39 | const boardScale = useSharedValue(0.95); 40 | const boardRotateZ = useSharedValue(0); 41 | const leavesRotateZ = useSharedValue(0); 42 | const pizzaScale = useSharedValue(0.95); 43 | const pizzaScalRotateZ = useSharedValue(0); 44 | 45 | const [size, setSize] = useState("m"); 46 | 47 | const xValue = useRef(0); 48 | const viewConfigRef = useRef({ itemVisiblePercentThreshold: 50 }); 49 | const onViewRef = useRef(({ viewableItems }: ViableItems) => { 50 | const currIndex = viewableItems[0]?.index ?? 0; 51 | 52 | setIndex(currIndex); 53 | }); 54 | 55 | const previowsIndex = index > 0 ? index - 1 : index; 56 | const previowsPrice = index === 0 ? 0 : pizzas[previowsIndex].prices[size]; 57 | const addPrice = aditionals.length > 0 ? aditionals[0].price : 0; 58 | const price = pizzas[index].prices[size] + addPrice; 59 | 60 | useEffect(() => { 61 | const prevIsLarge = boardScale.value > 1; 62 | const prevIsSmall = boardScale.value === 0.85; 63 | const transTime1 = { duration: 150 }; 64 | const transTime2 = { duration: 180 }; 65 | 66 | if (size === "s") { 67 | left.value = withTiming(0); 68 | pizzaScale.value = withTiming(prevIsLarge ? 1.08 : 0.98, transTime1); 69 | pizzaScale.value = withDelay(150, withTiming(0.85, transTime2)); 70 | boardScale.value = withDelay(300, withTiming(0.82, transTime2)); 71 | boardScale.value = withDelay(500, withTiming(0.85, transTime1)); 72 | } else if (size === "m") { 73 | left.value = withTiming(71); 74 | pizzaScale.value = withTiming(prevIsLarge ? 1.08 : 0.82, transTime1); 75 | pizzaScale.value = withDelay(150, withTiming(0.95, transTime2)); 76 | boardScale.value = withDelay(300, withTiming(prevIsLarge ? 0.9 : 0.98, transTime2)); 77 | boardScale.value = withDelay(500, withTiming(0.95, transTime1)); 78 | } else { 79 | left.value = withTiming(145); 80 | pizzaScale.value = withTiming(prevIsSmall ? 0.82 : 0.92, transTime1); 81 | pizzaScale.value = withDelay(150, withTiming(1.05, transTime2)); 82 | boardScale.value = withDelay(300, withTiming(1.08, transTime2)); 83 | boardScale.value = withDelay(500, withTiming(1.05, transTime1)); 84 | } 85 | }, [size]); 86 | 87 | function scrollHandler(event: NativeSyntheticEvent) { 88 | const { 89 | nativeEvent: { contentOffset }, 90 | } = event; 91 | const time = { duration: 100 }; 92 | 93 | if (contentOffset.x > 0 && contentOffset.x < 1242) { 94 | if (contentOffset.x > xValue.current) { 95 | boardRotateZ.value = withTiming(boardRotateZ.value - 2, time); 96 | boardRotateZ.value = withDelay(150, withTiming(boardRotateZ.value + 1.5, time)); 97 | boardRotateZ.value = withDelay(300, withTiming(boardRotateZ.value - 1.5, time)); 98 | leavesRotateZ.value = withTiming(leavesRotateZ.value - 1, time); 99 | leavesRotateZ.value = withDelay(150, withTiming(leavesRotateZ.value + 1, time)); 100 | pizzaScalRotateZ.value = withTiming(pizzaScalRotateZ.value - 7, time); 101 | } else if (contentOffset.x < xValue.current) { 102 | boardRotateZ.value = withTiming(boardRotateZ.value + 2, time); 103 | boardRotateZ.value = withDelay(150, withTiming(boardRotateZ.value - 1.5)); 104 | boardRotateZ.value = withDelay(300, withTiming(boardRotateZ.value + 1.5, time)); 105 | leavesRotateZ.value = withTiming(leavesRotateZ.value + 1, time); 106 | leavesRotateZ.value = withDelay(150, withTiming(leavesRotateZ.value - 1, time)); 107 | pizzaScalRotateZ.value = withTiming(pizzaScalRotateZ.value + 7, time); 108 | } 109 | } 110 | 111 | xValue.current = contentOffset.x; 112 | } 113 | 114 | const sliderAnimatedStyle = useAnimatedStyle(() => ({ 115 | left: left.value, 116 | })); 117 | 118 | const boardAnimatedStyle = useAnimatedStyle(() => ({ 119 | transform: [{ scale: boardScale.value }, { rotateZ: `${boardRotateZ.value}deg` }], 120 | })); 121 | 122 | const pizzaImageAnimatedStyle = useAnimatedStyle(() => ({ 123 | transform: [{ scale: pizzaScale.value }, { rotateZ: `${pizzaScalRotateZ.value}deg` }], 124 | })); 125 | 126 | const leavesImageAnimatedStyle = useAnimatedStyle(() => ({ 127 | transform: [{ rotateZ: `${leavesRotateZ.value}deg` }], 128 | })); 129 | 130 | return ( 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | pizza.name} 141 | disableIntervalMomentum 142 | pagingEnabled 143 | decelerationRate="fast" 144 | snapToAlignment="center" 145 | onViewableItemsChanged={onViewRef.current} 146 | viewabilityConfig={viewConfigRef.current} 147 | onScroll={scrollHandler} 148 | renderItem={({ item }) => ( 149 | 150 | 151 | 152 | 153 | )} 154 | showsHorizontalScrollIndicator={false} 155 | horizontal 156 | style={{ position: "absolute", overflow: "visible", width: "100%" }} 157 | /> 158 | 159 | 160 | $ 161 | previowsPrice ? price : previowsPrice} 165 | duration={1} 166 | isCounting 167 | formatter={(value) => value.toFixed(2)} 168 | /> 169 | 170 | 171 | setSize("s")}> 172 | S 173 | 174 | setSize("m")}> 175 | M 176 | 177 | setSize("l")}> 178 | L 179 | 180 | 181 | 182 | 183 | ); 184 | } 185 | --------------------------------------------------------------------------------