├── nativewind-env.d.ts ├── utilities ├── types.ts └── themeOptions.ts ├── assets ├── icon.png ├── splash.png ├── favicon.png ├── splash-dark.png └── adaptive-icon.png ├── tsconfig.json ├── babel.config.js ├── app ├── settings.tsx ├── shop │ └── _layout.tsx ├── (tabs) │ ├── index.tsx │ ├── _layout.tsx │ ├── profile.tsx │ └── chat.tsx └── _layout.tsx ├── metro.config.js ├── global.css ├── tailwind.config.js ├── components └── CustomText.tsx ├── eas.json ├── .gitignore ├── package.json ├── app.json └── README.md /nativewind-env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /utilities/types.ts: -------------------------------------------------------------------------------- 1 | type ThemeOptions = "light" | "dark" | "system" -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkbarBakhshi/RN-Journey/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkbarBakhshi/RN-Journey/HEAD/assets/splash.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkbarBakhshi/RN-Journey/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/splash-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkbarBakhshi/RN-Journey/HEAD/assets/splash-dark.png -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkbarBakhshi/RN-Journey/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: [ 5 | ["babel-preset-expo", { jsxImportSource: "nativewind" }], 6 | "nativewind/babel", 7 | ], 8 | }; 9 | }; -------------------------------------------------------------------------------- /app/settings.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | 3 | export default function Settings() { 4 | return ( 5 | 6 | Drawer [Settings] 7 | 8 | ); 9 | } -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require("expo/metro-config"); 2 | const { withNativeWind } = require('nativewind/metro'); 3 | 4 | const config = getDefaultConfig(__dirname) 5 | 6 | module.exports = withNativeWind(config, { input: './global.css' }) -------------------------------------------------------------------------------- /app/shop/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from "react-native"; 2 | 3 | export default function Shop() { 4 | return ( 5 | 6 | Drawer [Shop] 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /utilities/themeOptions.ts: -------------------------------------------------------------------------------- 1 | export const MyLightTheme = { 2 | dark: false, 3 | colors: { 4 | primary: "#5f0202", 5 | background: "#ffffff", 6 | card: "rgb(255, 255, 255)", 7 | text: "#5f0202", 8 | border: "#00ff00", 9 | notification: "rgb(55, 222, 58)", 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #000000; 8 | --primary: #5f0202; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background: #000000; 14 | --foreground: #ffffff; 15 | --primary: #0db4f7; 16 | } 17 | } -------------------------------------------------------------------------------- /app/(tabs)/index.tsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import CustomText from '../../components/CustomText'; 3 | 4 | export default function Dashboard() { 5 | return ( 6 | 7 | Update Tab [Dashboard] 8 | 9 | ); 10 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | // NOTE: Update this to include the paths to all of your component files. 4 | content: ["./app/**/*.{js,jsx,ts,tsx}"], 5 | presets: [require("nativewind/preset")], 6 | theme: { 7 | extend: { 8 | colors: { 9 | background: 'var(--background)', 10 | foreground: 'var(--foreground)', 11 | primary:'var(--primary)' 12 | }, 13 | }, 14 | }, 15 | plugins: [], 16 | } -------------------------------------------------------------------------------- /components/CustomText.tsx: -------------------------------------------------------------------------------- 1 | import { Text, TextProps, StyleProp, TextStyle } from "react-native"; 2 | import { ReactNode } from "react"; 3 | 4 | interface CustomTextProps extends TextProps { 5 | children: ReactNode; 6 | style?: StyleProp; 7 | } 8 | 9 | const CustomText = ({ children, style, ...props }: CustomTextProps) => { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export default CustomText; 18 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 7.8.2" 4 | }, 5 | "build": { 6 | "development": { 7 | "developmentClient": true, 8 | "distribution": "internal" 9 | }, 10 | "development-simulator": { 11 | "developmentClient": true, 12 | "distribution": "internal", 13 | "ios": { 14 | "simulator": true 15 | } 16 | }, 17 | "preview": { 18 | "distribution": "internal" 19 | }, 20 | "production": {} 21 | }, 22 | "submit": { 23 | "production": {} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | .env 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | 38 | google-services.json 39 | GoogleService-Info.plist 40 | 41 | ios 42 | android -------------------------------------------------------------------------------- /app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FontAwesome from '@expo/vector-icons/FontAwesome'; 3 | import { Tabs } from 'expo-router'; 4 | 5 | export default function TabLayout() { 6 | return ( 7 | 8 | 13 | }} 14 | /> 15 | 20 | }} 21 | /> 22 | , 27 | }} 28 | /> 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactnativejourney", 3 | "version": "1.0.0", 4 | "main": "expo-router/entry", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@expo-google-fonts/exo": "^0.2.3", 13 | "@react-native-async-storage/async-storage": "1.23.1", 14 | "@react-navigation/drawer": "^6.6.15", 15 | "@rneui/base": "^4.0.0-rc.7", 16 | "@rneui/themed": "^4.0.0-rc.8", 17 | "expo": "^51.0.8", 18 | "expo-constants": "~16.0.1", 19 | "expo-dev-client": "~4.0.14", 20 | "expo-font": "~12.0.5", 21 | "expo-linking": "~6.3.1", 22 | "expo-router": "~3.5.14", 23 | "expo-status-bar": "~1.12.1", 24 | "expo-system-ui": "~3.0.4", 25 | "nativewind": "^4.0.1", 26 | "react": "18.2.0", 27 | "react-native": "0.74.1", 28 | "react-native-gesture-handler": "~2.16.1", 29 | "react-native-keyboard-aware-scroll-view": "^0.9.5", 30 | "react-native-reanimated": "~3.10.1", 31 | "react-native-safe-area-context": "4.10.1", 32 | "react-native-screens": "3.31.1", 33 | "tailwindcss": "^3.4.3" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.20.0", 37 | "@types/react": "~18.2.45", 38 | "typescript": "~5.3.3" 39 | }, 40 | "private": true 41 | } 42 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "RNJourney", 4 | "slug": "RNJourney", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "automatic", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff", 13 | "dark": { 14 | "image": "./assets/splash.png", 15 | "resizeMode": "contain", 16 | "backgroundColor": "#000000" 17 | } 18 | }, 19 | "assetBundlePatterns": [ 20 | "**/*" 21 | ], 22 | "ios": { 23 | "supportsTablet": true, 24 | "bundleIdentifier": "com.rnjourney.RNJourney", 25 | "splash": { 26 | "image": "./assets/splash.png", 27 | "resizeMode": "contain", 28 | "backgroundColor": "#ffffff", 29 | "dark": { 30 | "image": "./assets/splash.png", 31 | "resizeMode": "contain", 32 | "backgroundColor": "#000000" 33 | } 34 | } 35 | }, 36 | "android": { 37 | "adaptiveIcon": { 38 | "foregroundImage": "./assets/adaptive-icon.png", 39 | "backgroundColor": "#ffffff" 40 | }, 41 | "package": "com.rnjourney.RNJourney", 42 | "splash": { 43 | "image": "./assets/splash.png", 44 | "resizeMode": "contain", 45 | "backgroundColor": "#ffffff", 46 | "dark": { 47 | "image": "./assets/splash-dark.png", 48 | "resizeMode": "contain", 49 | "backgroundColor": "#000000" 50 | } 51 | } 52 | }, 53 | "web": { 54 | "bundler": "metro", 55 | "favicon": "./assets/favicon.png" 56 | }, 57 | "plugins": [ 58 | "expo-router", 59 | "expo-font" 60 | ], 61 | "scheme": "rnjourney", 62 | "extra": { 63 | "router": { 64 | "origin": false 65 | }, 66 | "eas": { 67 | "projectId": "f3898dce-f56c-413e-8b84-dde1a0323b21" 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/(tabs)/profile.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from "react-native"; 2 | import { CheckBox } from "@rneui/themed"; 3 | import { useEffect, useState } from "react"; 4 | 5 | import { useColorScheme } from "nativewind"; 6 | import { MyLightTheme } from "../../utilities/themeOptions"; 7 | 8 | import { useTheme } from '@react-navigation/native'; 9 | 10 | import AsyncStorage from "@react-native-async-storage/async-storage"; 11 | 12 | export default function Profile() { 13 | const { setColorScheme, colorScheme } = useColorScheme(); 14 | const [selectedIndex, setSelectedIndex] = useState("light"); 15 | const toggleColorScheme = async (themeValue: ThemeOptions) => { 16 | setSelectedIndex(themeValue); 17 | setColorScheme(themeValue); 18 | await AsyncStorage.setItem("theme", themeValue); 19 | }; 20 | 21 | useEffect(() => { 22 | const getTheme = async () => { 23 | try { 24 | const themeValue = (await AsyncStorage.getItem( 25 | "theme" 26 | )) as ThemeOptions; 27 | if (themeValue) { 28 | setSelectedIndex(themeValue); 29 | } 30 | } catch (e) { 31 | console.log(e); 32 | } 33 | }; 34 | getTheme(); 35 | }, []); 36 | 37 | const { colors } = useTheme(); 38 | return ( 39 | 40 | toggleColorScheme("light")} 43 | checkedIcon="dot-circle-o" 44 | uncheckedIcon="circle-o" 45 | title="Light" 46 | className="bg-background" 47 | checkedColor={MyLightTheme.colors.primary} 48 | /> 49 | toggleColorScheme("dark")} 52 | checkedIcon="dot-circle-o" 53 | uncheckedIcon="circle-o" 54 | title="Dark" 55 | checkedColor={colors.notification} 56 | /> 57 | toggleColorScheme("system")} 60 | checkedIcon="dot-circle-o" 61 | uncheckedIcon="circle-o" 62 | title="System" 63 | checkedColor={colorScheme =="dark" ? colors.notification : MyLightTheme.colors.primary} 64 | /> 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 2 | import { Drawer } from "expo-router/drawer"; 3 | import { DarkTheme, ThemeProvider } from "@react-navigation/native"; 4 | import { useColorScheme } from "nativewind"; 5 | import { 6 | useFonts, 7 | Exo_400Regular, 8 | Exo_700Bold, 9 | Exo_900Black, 10 | } from "@expo-google-fonts/exo"; 11 | 12 | import { SplashScreen } from "expo-router"; 13 | 14 | import "../global.css"; 15 | import { MyLightTheme } from "../utilities/themeOptions"; 16 | import { useEffect } from "react"; 17 | import AsyncStorage from "@react-native-async-storage/async-storage"; 18 | 19 | SplashScreen.preventAutoHideAsync(); 20 | 21 | export default function Layout() { 22 | const { colorScheme, setColorScheme } = useColorScheme(); 23 | useEffect(() => { 24 | const loadTheme = async () => { 25 | // await AsyncStorage.removeItem('theme'); 26 | const stored = (await AsyncStorage.getItem("theme")) as ThemeOptions; 27 | if (stored) { 28 | setColorScheme(stored); 29 | } else { 30 | // Default to light if nothing or unexpected value is stored 31 | setColorScheme("light"); 32 | } 33 | }; 34 | 35 | loadTheme(); 36 | }, [colorScheme]); 37 | 38 | const [fontsLoaded, fontError] = useFonts({ 39 | Exo_400Regular, 40 | Exo_700Bold, 41 | Exo_900Black, 42 | }); 43 | 44 | useEffect(() => { 45 | if (fontsLoaded || fontError) { 46 | // Hide the splash screen after the fonts have loaded (or an error was returned) and the UI is ready. 47 | setTimeout(() => { 48 | SplashScreen.hideAsync(); 49 | }, 2000); 50 | } 51 | }, [fontsLoaded, fontError]); 52 | 53 | // Prevent rendering until the font has loaded or an error was returned 54 | if (!fontsLoaded && !fontError) { 55 | return null; 56 | } 57 | 58 | return ( 59 | 60 | 61 | 62 | 69 | 76 | 83 | 84 | 85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Upgrade to Expo51**: 2 | 1. ```npm install expo@latest``` - Upgrade expo to latest version (51 in our case) 3 | 2. ```npx expo install --fix``` - Resolve any dependency version compatibility issues 4 | 5 | **Here is a breakdown of the other branches:** 6 | 7 | 1. **Starter**: Tailwind CSS and Typescript setup. Corresponding video on YouTube: [React Native Expo with Typescript and Tailwind CSS (2024)](https://www.youtube.com/watch?v=9SdmwQPblBI) 8 | 9 | 2. **Features/ExpoRouter**: Nested routing with Expo Router V3 (Drawer and bottom tabs). Corresponding video on YouTube: [Expo Router V3 - Nested Bottom Tabs Inside Drawer Navigation](https://youtu.be/wOdz4XyMU7c) 10 | 11 | 3. **Features/Themes**: Themes with Expo Router v3 and NativeWind v4. Corresponding video on YouTube: [The best and easiest way to add a custom theme to Expo app with Expo Router (V3) and NativeWind (V4)](https://youtu.be/O6SYukr2zd0) 12 | 13 | 4. **Features/DevClient**: Expo Development Builds with Expo Dev Client. Corresponding video on YouTube: [Expo Development Build](https://youtu.be/C04MMx3ucO0) 14 | 15 | 5. **Features/Splash_Icon_Fonts**: Expo Splash screen (light and dark theme), App icon and Expo Google Fonts. Corresponding video on YouTube: [Expo Fonts, App Icon and Splash Screen (dark/light mode)](https://youtu.be/jEKdBqTclME) 16 | 17 | 6. **Features/ChatScreen**: Handling Keyboard display and hiding in React Native for TextInput so that the keyboard does not cover the text input area in iOS. Corresponding video on YouTube: [React Native Text Input iOS and Android - Handling Keyboard](https://youtu.be/DuspDGuR58Q) 18 | 19 | 7. **Features/Expo51_DevClient**: Upgrading Expo SDK 50 to SDK 51 w/development builds. Corresponding video on YouTube: [Ultimate Guide to Upgrade Expo SDK 50 to 51 - Managed workflow w/ Expo Go AND Development Builds](https://youtu.be/2gM8hg9W-J0) 20 | 21 | 8. **Features/IntervalTimer**: Build an interval timer (TABATA) in React Native. Use Expo Router, modal, Expo Haptics (vibrating device), Expo av (to make a beep sound), and more. Corresponding video on YouTube: [Build an Interval Timer in React Native (Expo Router, Navigation, Modal with Vibration and Audio)](https://youtu.be/ZU51gVaHHKs) 22 | 23 | 9. **Features/rnFirebase**: Step-by-step instructions to configure Firebase for your Expo app (Expo Routerv3). Corresponding video on YouTube: [Ultimate Firebase Setup for Expo in 2024: Step-by-Step Guide with Expo Router V3 & Full Auth Flow)](https://youtu.be/U5K-SfIOmZs) 24 | 25 | [Reset Password & Email Verification with Firebase in Expo App | Step-by-Step Guide](https://youtu.be/kCerC6XUbVc) 26 | 27 | 10. **Features/Sanity_CMS**: Stup Sanity CMS for your Expo React Native project. Corresponding video on YouTube: [How to Integrate Sanity CMS with React Native/Expo - Step-by-Step Guide](https://youtu.be/2sochWB3gjo) 28 | 29 | Make sure to update project name and database inside **sanity/sanity.config.ts** as well as **sanity.ts** files with your account details. 30 | 31 | 11. **Features/Dynamic_App_Icons**: Expo Prebuild demo using React Native Change Icon library [Expo Prebuild Showcase: Dynamically Change Application Icons in React Native](https://youtu.be/T7ybGL5HUhI) -------------------------------------------------------------------------------- /app/(tabs)/chat.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Pressable, 3 | TextInput, 4 | View, 5 | Animated, 6 | Keyboard, 7 | Easing, 8 | Platform, 9 | } from "react-native"; 10 | import CustomText from "../../components/CustomText"; 11 | import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; 12 | import { useEffect, useRef, useState } from "react"; 13 | import * as Linking from "expo-linking"; 14 | 15 | import { Feather } from "@expo/vector-icons"; 16 | import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs"; 17 | 18 | export default function Chat() { 19 | const [chatText, setChatText] = useState(""); 20 | const translateYRef = useRef(new Animated.Value(0)).current; 21 | const tabBarHeight = useBottomTabBarHeight(); 22 | const [textInputHeight, setTextInputHeight] = useState(60); 23 | 24 | const handleSubmit = () => { 25 | console.log(chatText); 26 | }; 27 | 28 | useEffect(() => { 29 | const keyboardWillShowListener = Keyboard.addListener( 30 | "keyboardWillShow", 31 | (event) => { 32 | const { height: newKeyboardHeight } = event.endCoordinates; 33 | Animated.timing(translateYRef, { 34 | toValue: tabBarHeight - newKeyboardHeight, // negative value of translateY means move up 35 | duration: event.duration, 36 | easing: Easing.bezier(0.33, 0.66, 0.66, 1), 37 | useNativeDriver: false, 38 | }).start(); 39 | } 40 | ); 41 | 42 | const keyboardWillHideListener = Keyboard.addListener( 43 | "keyboardWillHide", 44 | (event) => { 45 | Animated.timing(translateYRef, { 46 | toValue: 0, 47 | duration: event.duration, 48 | easing: Easing.bezier(0.33, 0.66, 0.66, 1), 49 | useNativeDriver: false, 50 | }).start(); 51 | } 52 | ); 53 | 54 | return () => { 55 | keyboardWillShowListener.remove(); 56 | keyboardWillHideListener.remove(); 57 | }; 58 | }, []); 59 | 60 | const handleContentSizeChange = (event: { 61 | nativeEvent: { contentSize: { height: number } }; 62 | }) => { 63 | setTextInputHeight(Math.max(event.nativeEvent.contentSize.height, 40)); 64 | }; 65 | 66 | return ( 67 | 68 | 72 | 73 | { 75 | Linking.openURL("https://youtu.be/nt4UQ_mzDeI"); 76 | }} 77 | > 78 | 79 | AI Chatbot with Resposne Streaming 80 | 81 | 82 | 83 | 84 | 94 | setChatText(text)} 100 | placeholder="Ask a question here" 101 | onContentSizeChange={handleContentSizeChange} 102 | /> 103 | 107 | 108 | 109 | 110 | 111 | ); 112 | } 113 | --------------------------------------------------------------------------------