├── store ├── index.ts └── store.ts ├── apis ├── index.ts └── pokemonApi │ ├── index.ts │ └── pokemonApi.ts ├── hooks ├── useColorScheme.ts ├── useColorScheme.web.ts └── useThemeColor.ts ├── assets ├── images │ ├── icon.png │ ├── favicon.png │ ├── react-logo.png │ ├── adaptive-icon.png │ ├── react-logo@2x.png │ ├── react-logo@3x.png │ ├── splash-icon.png │ └── partial-react-logo.png └── fonts │ └── SpaceMono-Regular.ttf ├── .prettierrc ├── .vscode └── settings.json ├── components ├── ui │ ├── TabBarBackground.tsx │ ├── TabBarBackground.ios.tsx │ ├── IconSymbol.ios.tsx │ └── IconSymbol.tsx ├── ThemedView.tsx ├── HapticTab.tsx ├── ExternalLink.tsx ├── HelloWave.tsx ├── Collapsible.tsx ├── ThemedText.tsx └── ParallaxScrollView.tsx ├── babel.config.js ├── eslint.config.js ├── tsconfig.json ├── .gitignore ├── constants └── Colors.ts ├── app ├── +not-found.tsx ├── _layout.tsx └── (tabs) │ ├── _layout.tsx │ ├── index.tsx │ └── explore.tsx ├── app.json ├── README.md ├── package.json └── scripts └── reset-project.js /store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './store'; 2 | -------------------------------------------------------------------------------- /apis/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pokemonApi'; 2 | -------------------------------------------------------------------------------- /apis/pokemonApi/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pokemonApi'; 2 | -------------------------------------------------------------------------------- /hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from 'react-native'; 2 | -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt-br/expo-paper-rtk/main/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt-br/expo-paper-rtk/main/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt-br/expo-paper-rtk/main/assets/images/react-logo.png -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt-br/expo-paper-rtk/main/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt-br/expo-paper-rtk/main/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt-br/expo-paper-rtk/main/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt-br/expo-paper-rtk/main/assets/images/splash-icon.png -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt-br/expo-paper-rtk/main/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt-br/expo-paper-rtk/main/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "explicit", 4 | "source.organizeImports": "explicit", 5 | "source.sortMembers": "explicit" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /components/ui/TabBarBackground.tsx: -------------------------------------------------------------------------------- 1 | // This is a shim for web and Android where the tab bar is generally opaque. 2 | export default undefined; 3 | 4 | export function useBottomTabOverflow() { 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | env: { 6 | production: { 7 | plugins: ['react-native-paper/babel'], 8 | }, 9 | }, 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // https://docs.expo.dev/guides/using-eslint/ 2 | const { defineConfig } = require('eslint/config'); 3 | const expoConfig = require('eslint-config-expo/flat'); 4 | 5 | module.exports = defineConfig([ 6 | expoConfig, 7 | { 8 | ignores: ['dist/*'], 9 | }, 10 | ]); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /components/ThemedView.tsx: -------------------------------------------------------------------------------- 1 | import { View, type ViewProps } from 'react-native'; 2 | 3 | import { useThemeColor } from '@/hooks/useThemeColor'; 4 | 5 | export type ThemedViewProps = ViewProps & { 6 | lightColor?: string; 7 | darkColor?: string; 8 | }; 9 | 10 | export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { 11 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); 12 | 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /hooks/useColorScheme.web.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useColorScheme as useRNColorScheme } from 'react-native'; 3 | 4 | /** 5 | * To support static rendering, this value needs to be re-calculated on the client side for web 6 | */ 7 | export function useColorScheme() { 8 | const [hasHydrated, setHasHydrated] = useState(false); 9 | 10 | useEffect(() => { 11 | setHasHydrated(true); 12 | }, []); 13 | 14 | const colorScheme = useRNColorScheme(); 15 | 16 | if (hasHydrated) { 17 | return colorScheme; 18 | } 19 | 20 | return 'light'; 21 | } 22 | -------------------------------------------------------------------------------- /.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 | expo-env.d.ts 11 | 12 | # Native 13 | .kotlin/ 14 | *.orig.* 15 | *.jks 16 | *.p8 17 | *.p12 18 | *.key 19 | *.mobileprovision 20 | 21 | # Metro 22 | .metro-health-check* 23 | 24 | # debug 25 | npm-debug.* 26 | yarn-debug.* 27 | yarn-error.* 28 | 29 | # macOS 30 | .DS_Store 31 | *.pem 32 | 33 | # local env files 34 | .env*.local 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | 39 | app-example 40 | -------------------------------------------------------------------------------- /hooks/useThemeColor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about light and dark modes: 3 | * https://docs.expo.dev/guides/color-schemes/ 4 | */ 5 | 6 | import { Colors } from '@/constants/Colors'; 7 | import { useColorScheme } from '@/hooks/useColorScheme'; 8 | 9 | export function useThemeColor( 10 | props: { light?: string; dark?: string }, 11 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark 12 | ) { 13 | const theme = useColorScheme() ?? 'light'; 14 | const colorFromProps = props[theme]; 15 | 16 | if (colorFromProps) { 17 | return colorFromProps; 18 | } else { 19 | return Colors[theme][colorName]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/HapticTab.tsx: -------------------------------------------------------------------------------- 1 | import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs'; 2 | import { PlatformPressable } from '@react-navigation/elements'; 3 | import * as Haptics from 'expo-haptics'; 4 | 5 | export function HapticTab(props: BottomTabBarButtonProps) { 6 | return ( 7 | { 10 | if (process.env.EXPO_OS === 'ios') { 11 | // Add a soft haptic feedback when pressing down on the tabs. 12 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); 13 | } 14 | props.onPressIn?.(ev); 15 | }} 16 | /> 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/ui/TabBarBackground.ios.tsx: -------------------------------------------------------------------------------- 1 | import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; 2 | import { BlurView } from 'expo-blur'; 3 | import { StyleSheet } from 'react-native'; 4 | 5 | export default function BlurTabBarBackground() { 6 | return ( 7 | 14 | ); 15 | } 16 | 17 | export function useBottomTabOverflow() { 18 | return useBottomTabBarHeight(); 19 | } 20 | -------------------------------------------------------------------------------- /apis/pokemonApi/pokemonApi.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; 2 | // import type { Pokemon } from './types'; 3 | 4 | // Define a service using a base URL and expected endpoints 5 | export const pokemonApi = createApi({ 6 | reducerPath: 'pokemonApi', 7 | baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), 8 | endpoints: (build) => ({ 9 | getPokemonByName: build.query({ 10 | query: (name) => `pokemon/${name}`, 11 | }), 12 | }), 13 | }); 14 | 15 | // Export hooks for usage in functional components, which are 16 | // auto-generated based on the defined endpoints 17 | export const { useGetPokemonByNameQuery } = pokemonApi; 18 | -------------------------------------------------------------------------------- /components/ui/IconSymbol.ios.tsx: -------------------------------------------------------------------------------- 1 | import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols'; 2 | import { StyleProp, ViewStyle } from 'react-native'; 3 | 4 | export function IconSymbol({ 5 | name, 6 | size = 24, 7 | color, 8 | style, 9 | weight = 'regular', 10 | }: { 11 | name: SymbolViewProps['name']; 12 | size?: number; 13 | color: string; 14 | style?: StyleProp; 15 | weight?: SymbolWeight; 16 | }) { 17 | return ( 18 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import { Href, Link } from 'expo-router'; 2 | import { openBrowserAsync } from 'expo-web-browser'; 3 | import { type ComponentProps } from 'react'; 4 | import { Platform } from 'react-native'; 5 | 6 | type Props = Omit, 'href'> & { href: Href & string }; 7 | 8 | export function ExternalLink({ href, ...rest }: Props) { 9 | return ( 10 | { 15 | if (Platform.OS !== 'web') { 16 | // Prevent the default behavior of linking to the default browser on native. 17 | event.preventDefault(); 18 | // Open the link in an in-app browser. 19 | await openBrowserAsync(href); 20 | } 21 | }} 22 | /> 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /store/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | // Or from '@reduxjs/toolkit/query/react' 3 | import { setupListeners } from '@reduxjs/toolkit/query'; 4 | 5 | import { pokemonApi } from '@/apis'; 6 | 7 | export const store = configureStore({ 8 | reducer: { 9 | // Add the generated reducer as a specific top-level slice 10 | [pokemonApi.reducerPath]: pokemonApi.reducer, 11 | }, 12 | // Adding the api middleware enables caching, invalidation, polling, 13 | // and other useful features of `rtk-query`. 14 | middleware: (getDefaultMiddleware) => 15 | getDefaultMiddleware().concat(pokemonApi.middleware), 16 | }); 17 | 18 | // optional, but required for refetchOnFocus/refetchOnReconnect behaviors 19 | // see `setupListeners` docs - takes an optional callback as the 2nd arg for customization 20 | setupListeners(store.dispatch); 21 | -------------------------------------------------------------------------------- /constants/Colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode. 3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. 4 | */ 5 | 6 | const tintColorLight = '#0a7ea4'; 7 | const tintColorDark = '#fff'; 8 | 9 | export const Colors = { 10 | light: { 11 | text: '#11181C', 12 | background: '#fff', 13 | tint: tintColorLight, 14 | icon: '#687076', 15 | tabIconDefault: '#687076', 16 | tabIconSelected: tintColorLight, 17 | }, 18 | dark: { 19 | text: '#ECEDEE', 20 | background: '#151718', 21 | tint: tintColorDark, 22 | icon: '#9BA1A6', 23 | tabIconDefault: '#9BA1A6', 24 | tabIconSelected: tintColorDark, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /app/+not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Stack } from 'expo-router'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import { ThemedText } from '@/components/ThemedText'; 5 | import { ThemedView } from '@/components/ThemedView'; 6 | 7 | export default function NotFoundScreen() { 8 | return ( 9 | <> 10 | 11 | 12 | This screen does not exist. 13 | 14 | Go to home screen! 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | flex: 1, 24 | alignItems: 'center', 25 | justifyContent: 'center', 26 | padding: 20, 27 | }, 28 | link: { 29 | marginTop: 15, 30 | paddingVertical: 15, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "expo-app", 4 | "slug": "expo-app", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "expoapp", 9 | "userInterfaceStyle": "automatic", 10 | "newArchEnabled": true, 11 | "ios": { 12 | "supportsTablet": true 13 | }, 14 | "android": { 15 | "adaptiveIcon": { 16 | "foregroundImage": "./assets/images/adaptive-icon.png", 17 | "backgroundColor": "#ffffff" 18 | }, 19 | "edgeToEdgeEnabled": true 20 | }, 21 | "web": { 22 | "bundler": "metro", 23 | "output": "static", 24 | "favicon": "./assets/images/favicon.png" 25 | }, 26 | "plugins": [ 27 | "expo-router", 28 | [ 29 | "expo-splash-screen", 30 | { 31 | "image": "./assets/images/splash-icon.png", 32 | "imageWidth": 200, 33 | "resizeMode": "contain", 34 | "backgroundColor": "#ffffff" 35 | } 36 | ] 37 | ], 38 | "experiments": { 39 | "typedRoutes": true 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /components/HelloWave.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import Animated, { 4 | useAnimatedStyle, 5 | useSharedValue, 6 | withRepeat, 7 | withSequence, 8 | withTiming, 9 | } from 'react-native-reanimated'; 10 | 11 | import { ThemedText } from '@/components/ThemedText'; 12 | 13 | export function HelloWave() { 14 | const rotationAnimation = useSharedValue(0); 15 | 16 | useEffect(() => { 17 | rotationAnimation.value = withRepeat( 18 | withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })), 19 | 4 // Run the animation 4 times 20 | ); 21 | }, [rotationAnimation]); 22 | 23 | const animatedStyle = useAnimatedStyle(() => ({ 24 | transform: [{ rotate: `${rotationAnimation.value}deg` }], 25 | })); 26 | 27 | return ( 28 | 29 | 👋 30 | 31 | ); 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | text: { 36 | fontSize: 28, 37 | lineHeight: 32, 38 | marginTop: -6, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DarkTheme, 3 | DefaultTheme, 4 | ThemeProvider, 5 | } from '@react-navigation/native'; 6 | import { useFonts } from 'expo-font'; 7 | import { Stack } from 'expo-router'; 8 | import { StatusBar } from 'expo-status-bar'; 9 | import { PaperProvider } from 'react-native-paper'; 10 | import 'react-native-reanimated'; 11 | import { Provider } from 'react-redux'; 12 | 13 | import { useColorScheme } from '@/hooks/useColorScheme'; 14 | import { store } from '@/store'; 15 | 16 | export default function RootLayout() { 17 | const colorScheme = useColorScheme(); 18 | const [loaded] = useFonts({ 19 | SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), 20 | }); 21 | 22 | if (!loaded) { 23 | // Async font loading only occurs in development. 24 | return null; 25 | } 26 | 27 | return ( 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from 'expo-router'; 2 | import React from 'react'; 3 | import { Platform } from 'react-native'; 4 | 5 | import { HapticTab } from '@/components/HapticTab'; 6 | import { IconSymbol } from '@/components/ui/IconSymbol'; 7 | import TabBarBackground from '@/components/ui/TabBarBackground'; 8 | import { Colors } from '@/constants/Colors'; 9 | import { useColorScheme } from '@/hooks/useColorScheme'; 10 | 11 | export default function TabLayout() { 12 | const colorScheme = useColorScheme(); 13 | 14 | return ( 15 | 29 | , 34 | }} 35 | /> 36 | , 41 | }} 42 | /> 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /components/Collapsible.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, useState } from 'react'; 2 | import { StyleSheet, TouchableOpacity } from 'react-native'; 3 | 4 | import { ThemedText } from '@/components/ThemedText'; 5 | import { ThemedView } from '@/components/ThemedView'; 6 | import { IconSymbol } from '@/components/ui/IconSymbol'; 7 | import { Colors } from '@/constants/Colors'; 8 | import { useColorScheme } from '@/hooks/useColorScheme'; 9 | 10 | export function Collapsible({ children, title }: PropsWithChildren & { title: string }) { 11 | const [isOpen, setIsOpen] = useState(false); 12 | const theme = useColorScheme() ?? 'light'; 13 | 14 | return ( 15 | 16 | setIsOpen((value) => !value)} 19 | activeOpacity={0.8}> 20 | 27 | 28 | {title} 29 | 30 | {isOpen && {children}} 31 | 32 | ); 33 | } 34 | 35 | const styles = StyleSheet.create({ 36 | heading: { 37 | flexDirection: 'row', 38 | alignItems: 'center', 39 | gap: 6, 40 | }, 41 | content: { 42 | marginTop: 6, 43 | marginLeft: 24, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /components/ThemedText.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Text, type TextProps } from 'react-native'; 2 | 3 | import { useThemeColor } from '@/hooks/useThemeColor'; 4 | 5 | export type ThemedTextProps = TextProps & { 6 | lightColor?: string; 7 | darkColor?: string; 8 | type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'; 9 | }; 10 | 11 | export function ThemedText({ 12 | style, 13 | lightColor, 14 | darkColor, 15 | type = 'default', 16 | ...rest 17 | }: ThemedTextProps) { 18 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); 19 | 20 | return ( 21 | 33 | ); 34 | } 35 | 36 | const styles = StyleSheet.create({ 37 | default: { 38 | fontSize: 16, 39 | lineHeight: 24, 40 | }, 41 | defaultSemiBold: { 42 | fontSize: 16, 43 | lineHeight: 24, 44 | fontWeight: '600', 45 | }, 46 | title: { 47 | fontSize: 32, 48 | fontWeight: 'bold', 49 | lineHeight: 32, 50 | }, 51 | subtitle: { 52 | fontSize: 20, 53 | fontWeight: 'bold', 54 | }, 55 | link: { 56 | lineHeight: 30, 57 | fontSize: 16, 58 | color: '#0a7ea4', 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /components/ui/IconSymbol.tsx: -------------------------------------------------------------------------------- 1 | // Fallback for using MaterialIcons on Android and web. 2 | 3 | import MaterialIcons from '@expo/vector-icons/MaterialIcons'; 4 | import { SymbolWeight, SymbolViewProps } from 'expo-symbols'; 5 | import { ComponentProps } from 'react'; 6 | import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native'; 7 | 8 | type IconMapping = Record['name']>; 9 | type IconSymbolName = keyof typeof MAPPING; 10 | 11 | /** 12 | * Add your SF Symbols to Material Icons mappings here. 13 | * - see Material Icons in the [Icons Directory](https://icons.expo.fyi). 14 | * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app. 15 | */ 16 | const MAPPING = { 17 | 'house.fill': 'home', 18 | 'paperplane.fill': 'send', 19 | 'chevron.left.forwardslash.chevron.right': 'code', 20 | 'chevron.right': 'chevron-right', 21 | } as IconMapping; 22 | 23 | /** 24 | * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web. 25 | * This ensures a consistent look across platforms, and optimal resource usage. 26 | * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons. 27 | */ 28 | export function IconSymbol({ 29 | name, 30 | size = 24, 31 | color, 32 | style, 33 | }: { 34 | name: IconSymbolName; 35 | size?: number; 36 | color: string | OpaqueColorValue; 37 | style?: StyleProp; 38 | weight?: SymbolWeight; 39 | }) { 40 | return ; 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Expo app 👋 2 | 3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). 4 | 5 | ## Get started 6 | 7 | 1. Install dependencies 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | 2. Start the app 14 | 15 | ```bash 16 | npx expo start 17 | ``` 18 | 19 | In the output, you'll find options to open the app in a 20 | 21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/) 22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) 23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) 24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo 25 | 26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). 27 | 28 | ## Get a fresh project 29 | 30 | When you're ready, run: 31 | 32 | ```bash 33 | npm run reset-project 34 | ``` 35 | 36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. 37 | 38 | ## Learn more 39 | 40 | To learn more about developing your project with Expo, look at the following resources: 41 | 42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). 43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. 44 | 45 | ## Join the community 46 | 47 | Join our community of developers creating universal apps. 48 | 49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. 50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-app", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "reset-project": "node ./scripts/reset-project.js", 8 | "android": "expo start --android", 9 | "ios": "expo start --ios", 10 | "web": "expo start --web", 11 | "lint": "expo lint" 12 | }, 13 | "dependencies": { 14 | "@expo/vector-icons": "^14.1.0", 15 | "@react-native-vector-icons/material-design-icons": "^12.0.1", 16 | "@react-navigation/bottom-tabs": "^7.3.10", 17 | "@react-navigation/elements": "^2.3.8", 18 | "@react-navigation/native": "^7.1.6", 19 | "@reduxjs/toolkit": "^2.8.2", 20 | "expo": "~53.0.17", 21 | "expo-blur": "~14.1.5", 22 | "expo-constants": "~17.1.7", 23 | "expo-font": "~13.3.2", 24 | "expo-haptics": "~14.1.4", 25 | "expo-image": "~2.3.2", 26 | "expo-linking": "~7.1.7", 27 | "expo-router": "~5.1.3", 28 | "expo-splash-screen": "~0.30.10", 29 | "expo-status-bar": "~2.2.3", 30 | "expo-symbols": "~0.4.5", 31 | "expo-system-ui": "~5.0.10", 32 | "expo-web-browser": "~14.2.0", 33 | "react": "19.0.0", 34 | "react-dom": "19.0.0", 35 | "react-native": "0.79.5", 36 | "react-native-gesture-handler": "~2.24.0", 37 | "react-native-paper": "^5.14.5", 38 | "react-native-reanimated": "~3.17.4", 39 | "react-native-safe-area-context": "^5.4.0", 40 | "react-native-screens": "~4.11.1", 41 | "react-native-web": "~0.20.0", 42 | "react-native-webview": "13.13.5", 43 | "react-redux": "^9.2.0" 44 | }, 45 | "devDependencies": { 46 | "@babel/core": "^7.25.2", 47 | "@types/react": "~19.0.10", 48 | "eslint": "^9.25.0", 49 | "eslint-config-expo": "~9.2.0", 50 | "husky": "^9.1.7", 51 | "lint-staged": "^16.1.2", 52 | "typescript": "~5.8.3" 53 | }, 54 | "husky": { 55 | "hooks": { 56 | "pre-commit": "lint-staged" 57 | } 58 | }, 59 | "lint-staged": { 60 | "src/**/!(*test).js": [ 61 | "prettier --single-quote --trailing-comma es5 --write", 62 | "eslint --fix" 63 | ] 64 | }, 65 | "private": true 66 | } 67 | -------------------------------------------------------------------------------- /components/ParallaxScrollView.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren, ReactElement } from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import Animated, { 4 | interpolate, 5 | useAnimatedRef, 6 | useAnimatedStyle, 7 | useScrollViewOffset, 8 | } from 'react-native-reanimated'; 9 | 10 | import { ThemedView } from '@/components/ThemedView'; 11 | import { useBottomTabOverflow } from '@/components/ui/TabBarBackground'; 12 | import { useColorScheme } from '@/hooks/useColorScheme'; 13 | 14 | const HEADER_HEIGHT = 250; 15 | 16 | type Props = PropsWithChildren<{ 17 | headerImage: ReactElement; 18 | headerBackgroundColor: { dark: string; light: string }; 19 | }>; 20 | 21 | export default function ParallaxScrollView({ 22 | children, 23 | headerImage, 24 | headerBackgroundColor, 25 | }: Props) { 26 | const colorScheme = useColorScheme() ?? 'light'; 27 | const scrollRef = useAnimatedRef(); 28 | const scrollOffset = useScrollViewOffset(scrollRef); 29 | const bottom = useBottomTabOverflow(); 30 | const headerAnimatedStyle = useAnimatedStyle(() => { 31 | return { 32 | transform: [ 33 | { 34 | translateY: interpolate( 35 | scrollOffset.value, 36 | [-HEADER_HEIGHT, 0, HEADER_HEIGHT], 37 | [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75] 38 | ), 39 | }, 40 | { 41 | scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]), 42 | }, 43 | ], 44 | }; 45 | }); 46 | 47 | return ( 48 | 49 | 54 | 60 | {headerImage} 61 | 62 | {children} 63 | 64 | 65 | ); 66 | } 67 | 68 | const styles = StyleSheet.create({ 69 | container: { 70 | flex: 1, 71 | }, 72 | header: { 73 | height: HEADER_HEIGHT, 74 | overflow: 'hidden', 75 | }, 76 | content: { 77 | flex: 1, 78 | padding: 32, 79 | gap: 16, 80 | overflow: 'hidden', 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /app/(tabs)/index.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from 'expo-image'; 2 | import { Platform, StyleSheet } from 'react-native'; 3 | import { ActivityIndicator } from 'react-native-paper'; 4 | 5 | import { useGetPokemonByNameQuery } from '@/apis'; 6 | 7 | import { HelloWave } from '@/components/HelloWave'; 8 | import ParallaxScrollView from '@/components/ParallaxScrollView'; 9 | import { ThemedText } from '@/components/ThemedText'; 10 | import { ThemedView } from '@/components/ThemedView'; 11 | 12 | export default function HomeScreen() { 13 | const { data, isLoading, error } = useGetPokemonByNameQuery('bulbasaur'); 14 | 15 | console.log('### data >>>>>', data); 16 | 17 | return ( 18 | 25 | } 26 | > 27 | 28 | Welcome! 29 | 30 | 31 | 32 | 33 | Step 1: Try it 34 | 35 | Edit{' '} 36 | app/(tabs)/index.tsx{' '} 37 | to see changes. Press{' '} 38 | 39 | {Platform.select({ 40 | ios: 'cmd + d', 41 | android: 'cmd + m', 42 | web: 'F12', 43 | })} 44 | {' '} 45 | to open developer tools. 46 | 47 | 48 | 49 | Step 2: Explore 50 | 51 | {`Tap the Explore tab to learn more about what's included in this starter app.`} 52 | 53 | 54 | 55 | Step 3: Get a fresh start 56 | 57 | {`When you're ready, run `} 58 | 59 | npm run reset-project 60 | {' '} 61 | to get a fresh app{' '} 62 | directory. This will move the current{' '} 63 | app to{' '} 64 | app-example. 65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | const styles = StyleSheet.create({ 72 | titleContainer: { 73 | flexDirection: 'row', 74 | alignItems: 'center', 75 | gap: 8, 76 | }, 77 | stepContainer: { 78 | gap: 8, 79 | marginBottom: 8, 80 | }, 81 | reactLogo: { 82 | height: 178, 83 | width: 290, 84 | bottom: 0, 85 | left: 0, 86 | position: 'absolute', 87 | }, 88 | }); 89 | -------------------------------------------------------------------------------- /scripts/reset-project.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script is used to reset the project to a blank state. 5 | * It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file. 6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it. 7 | */ 8 | 9 | const fs = require("fs"); 10 | const path = require("path"); 11 | const readline = require("readline"); 12 | 13 | const root = process.cwd(); 14 | const oldDirs = ["app", "components", "hooks", "constants", "scripts"]; 15 | const exampleDir = "app-example"; 16 | const newAppDir = "app"; 17 | const exampleDirPath = path.join(root, exampleDir); 18 | 19 | const indexContent = `import { Text, View } from "react-native"; 20 | 21 | export default function Index() { 22 | return ( 23 | 30 | Edit app/index.tsx to edit this screen. 31 | 32 | ); 33 | } 34 | `; 35 | 36 | const layoutContent = `import { Stack } from "expo-router"; 37 | 38 | export default function RootLayout() { 39 | return ; 40 | } 41 | `; 42 | 43 | const rl = readline.createInterface({ 44 | input: process.stdin, 45 | output: process.stdout, 46 | }); 47 | 48 | const moveDirectories = async (userInput) => { 49 | try { 50 | if (userInput === "y") { 51 | // Create the app-example directory 52 | await fs.promises.mkdir(exampleDirPath, { recursive: true }); 53 | console.log(`📁 /${exampleDir} directory created.`); 54 | } 55 | 56 | // Move old directories to new app-example directory or delete them 57 | for (const dir of oldDirs) { 58 | const oldDirPath = path.join(root, dir); 59 | if (fs.existsSync(oldDirPath)) { 60 | if (userInput === "y") { 61 | const newDirPath = path.join(root, exampleDir, dir); 62 | await fs.promises.rename(oldDirPath, newDirPath); 63 | console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`); 64 | } else { 65 | await fs.promises.rm(oldDirPath, { recursive: true, force: true }); 66 | console.log(`❌ /${dir} deleted.`); 67 | } 68 | } else { 69 | console.log(`➡️ /${dir} does not exist, skipping.`); 70 | } 71 | } 72 | 73 | // Create new /app directory 74 | const newAppDirPath = path.join(root, newAppDir); 75 | await fs.promises.mkdir(newAppDirPath, { recursive: true }); 76 | console.log("\n📁 New /app directory created."); 77 | 78 | // Create index.tsx 79 | const indexPath = path.join(newAppDirPath, "index.tsx"); 80 | await fs.promises.writeFile(indexPath, indexContent); 81 | console.log("📄 app/index.tsx created."); 82 | 83 | // Create _layout.tsx 84 | const layoutPath = path.join(newAppDirPath, "_layout.tsx"); 85 | await fs.promises.writeFile(layoutPath, layoutContent); 86 | console.log("📄 app/_layout.tsx created."); 87 | 88 | console.log("\n✅ Project reset complete. Next steps:"); 89 | console.log( 90 | `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${ 91 | userInput === "y" 92 | ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` 93 | : "" 94 | }` 95 | ); 96 | } catch (error) { 97 | console.error(`❌ Error during script execution: ${error.message}`); 98 | } 99 | }; 100 | 101 | rl.question( 102 | "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ", 103 | (answer) => { 104 | const userInput = answer.trim().toLowerCase() || "y"; 105 | if (userInput === "y" || userInput === "n") { 106 | moveDirectories(userInput).finally(() => rl.close()); 107 | } else { 108 | console.log("❌ Invalid input. Please enter 'Y' or 'N'."); 109 | rl.close(); 110 | } 111 | } 112 | ); 113 | -------------------------------------------------------------------------------- /app/(tabs)/explore.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from 'expo-image'; 2 | import { Platform, StyleSheet } from 'react-native'; 3 | 4 | import { Collapsible } from '@/components/Collapsible'; 5 | import { ExternalLink } from '@/components/ExternalLink'; 6 | import ParallaxScrollView from '@/components/ParallaxScrollView'; 7 | import { ThemedText } from '@/components/ThemedText'; 8 | import { ThemedView } from '@/components/ThemedView'; 9 | import { IconSymbol } from '@/components/ui/IconSymbol'; 10 | 11 | export default function TabTwoScreen() { 12 | return ( 13 | 22 | }> 23 | 24 | Explore 25 | 26 | This app includes example code to help you get started. 27 | 28 | 29 | This app has two screens:{' '} 30 | app/(tabs)/index.tsx and{' '} 31 | app/(tabs)/explore.tsx 32 | 33 | 34 | The layout file in app/(tabs)/_layout.tsx{' '} 35 | sets up the tab navigator. 36 | 37 | 38 | Learn more 39 | 40 | 41 | 42 | 43 | You can open this project on Android, iOS, and the web. To open the web version, press{' '} 44 | w in the terminal running this project. 45 | 46 | 47 | 48 | 49 | For static images, you can use the @2x and{' '} 50 | @3x suffixes to provide files for 51 | different screen densities 52 | 53 | 54 | 55 | Learn more 56 | 57 | 58 | 59 | 60 | Open app/_layout.tsx to see how to load{' '} 61 | 62 | custom fonts such as this one. 63 | 64 | 65 | 66 | Learn more 67 | 68 | 69 | 70 | 71 | This template has light and dark mode support. The{' '} 72 | useColorScheme() hook lets you inspect 73 | what the user's current color scheme is, and so you can adjust UI colors accordingly. 74 | 75 | 76 | Learn more 77 | 78 | 79 | 80 | 81 | This template includes an example of an animated component. The{' '} 82 | components/HelloWave.tsx component uses 83 | the powerful react-native-reanimated{' '} 84 | library to create a waving hand animation. 85 | 86 | {Platform.select({ 87 | ios: ( 88 | 89 | The components/ParallaxScrollView.tsx{' '} 90 | component provides a parallax effect for the header image. 91 | 92 | ), 93 | })} 94 | 95 | 96 | ); 97 | } 98 | 99 | const styles = StyleSheet.create({ 100 | headerImage: { 101 | color: '#808080', 102 | bottom: -90, 103 | left: -35, 104 | position: 'absolute', 105 | }, 106 | titleContainer: { 107 | flexDirection: 'row', 108 | gap: 8, 109 | }, 110 | }); 111 | --------------------------------------------------------------------------------