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