diff --git a/packages/expo-template/README.md b/packages/expo-template/README.md new file mode 100644 index 0000000..48dd63f --- /dev/null +++ b/packages/expo-template/README.md @@ -0,0 +1,50 @@ +# Welcome to your Expo app šŸ‘‹ + +This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). + +## Get started + +1. Install dependencies + + ```bash + npm install + ``` + +2. Start the app + + ```bash + npx expo start + ``` + +In the output, you'll find options to open the app in a + +- [development build](https://docs.expo.dev/develop/development-builds/introduction/) +- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) +- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) +- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo + +You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). + +## Get a fresh project + +When you're ready, run: + +```bash +npm run reset-project +``` + +This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. + +## Learn more + +To learn more about developing your project with Expo, look at the following resources: + +- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). +- [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. + +## Join the community + +Join our community of developers creating universal apps. + +- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. +- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. diff --git a/packages/expo-template/app.json b/packages/expo-template/app.json new file mode 100644 index 0000000..6ba124e --- /dev/null +++ b/packages/expo-template/app.json @@ -0,0 +1,41 @@ +{ + "expo": { + "name": "HelloWorld", + "slug": "expo-template-default", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "myapp", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/images/adaptive-icon.png", + "backgroundColor": "#ffffff" + } + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + [ + "expo-splash-screen", + { + "image": "./assets/images/splash-icon.png", + "imageWidth": 200, + "resizeMode": "contain", + "backgroundColor": "#ffffff" + } + ] + ], + "experiments": { + "typedRoutes": true + } + } +} \ No newline at end of file diff --git a/packages/expo-template/app/(tabs)/_layout.tsx b/packages/expo-template/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..d1d39bd --- /dev/null +++ b/packages/expo-template/app/(tabs)/_layout.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { withLayoutContext } from 'expo-router'; +import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'; + +const Tabs = withLayoutContext(createNativeBottomTabNavigator().Navigator); + +export default function TabLayout() { + return ( + + ({ sfSymbol: 'house.fill' }), + }} + /> + ({ sfSymbol: 'paperplane.fill' }), + }} + /> + + ); +} diff --git a/packages/expo-template/app/(tabs)/explore.tsx b/packages/expo-template/app/(tabs)/explore.tsx new file mode 100644 index 0000000..9c9715d --- /dev/null +++ b/packages/expo-template/app/(tabs)/explore.tsx @@ -0,0 +1,129 @@ +import { StyleSheet, Image, Platform } from 'react-native'; + +import { Collapsible } from '@/components/Collapsible'; +import { ExternalLink } from '@/components/ExternalLink'; +import ParallaxScrollView from '@/components/ParallaxScrollView'; +import { ThemedText } from '@/components/ThemedText'; +import { ThemedView } from '@/components/ThemedView'; +import { IconSymbol } from '@/components/ui/IconSymbol'; + +export default function TabTwoScreen() { + return ( + + } + > + + Explore + + + This app includes example code to help you get started. + + + + This app has two screens:{' '} + app/(tabs)/index.tsx{' '} + and{' '} + app/(tabs)/explore.tsx + + + The layout file in{' '} + app/(tabs)/_layout.tsx{' '} + sets up the tab navigator. + + + Learn more + + + + + You can open this project on Android, iOS, and the web. To open the + web version, press w{' '} + in the terminal running this project. + + + + + For static images, you can use the{' '} + @2x and{' '} + @3x suffixes to + provide files for different screen densities + + + + Learn more + + + + + Open app/_layout.tsx{' '} + to see how to load{' '} + + custom fonts such as this one. + + + + Learn more + + + + + This template has light and dark mode support. The{' '} + useColorScheme() hook + lets you inspect what the user's current color scheme is, and so you + can adjust UI colors accordingly. + + + Learn more + + + + + This template includes an example of an animated component. The{' '} + + components/HelloWave.tsx + {' '} + component uses the powerful{' '} + + react-native-reanimated + {' '} + library to create a waving hand animation. + + {Platform.select({ + ios: ( + + The{' '} + + components/ParallaxScrollView.tsx + {' '} + component provides a parallax effect for the header image. + + ), + })} + + + ); +} + +const styles = StyleSheet.create({ + headerImage: { + color: '#808080', + bottom: -90, + left: -35, + position: 'absolute', + }, + titleContainer: { + flexDirection: 'row', + gap: 8, + }, +}); diff --git a/packages/expo-template/app/(tabs)/index.tsx b/packages/expo-template/app/(tabs)/index.tsx new file mode 100644 index 0000000..08d122b --- /dev/null +++ b/packages/expo-template/app/(tabs)/index.tsx @@ -0,0 +1,78 @@ +import { Image, StyleSheet, Platform } from 'react-native'; + +import { HelloWave } from '@/components/HelloWave'; +import ParallaxScrollView from '@/components/ParallaxScrollView'; +import { ThemedText } from '@/components/ThemedText'; +import { ThemedView } from '@/components/ThemedView'; + +export default function HomeScreen() { + return ( + + } + > + + Welcome! + + + + Step 1: Try it + + Edit{' '} + app/(tabs)/index.tsx{' '} + to see changes. Press{' '} + + {Platform.select({ + ios: 'cmd + d', + android: 'cmd + m', + web: 'F12', + })} + {' '} + to open developer tools. + + + + Step 2: Explore + + Tap the Explore tab to learn more about what's included in this + starter app. + + + + Step 3: Get a fresh start + + When you're ready, run{' '} + npm run reset-project{' '} + to get a fresh app{' '} + directory. This will move the current{' '} + app to{' '} + app-example. + + + + ); +} + +const styles = StyleSheet.create({ + titleContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + stepContainer: { + gap: 8, + marginBottom: 8, + }, + reactLogo: { + height: 178, + width: 290, + bottom: 0, + left: 0, + position: 'absolute', + }, +}); diff --git a/packages/expo-template/app/+not-found.tsx b/packages/expo-template/app/+not-found.tsx new file mode 100644 index 0000000..963b04f --- /dev/null +++ b/packages/expo-template/app/+not-found.tsx @@ -0,0 +1,32 @@ +import { Link, Stack } from 'expo-router'; +import { StyleSheet } from 'react-native'; + +import { ThemedText } from '@/components/ThemedText'; +import { ThemedView } from '@/components/ThemedView'; + +export default function NotFoundScreen() { + return ( + <> + + + This screen doesn't exist. + + Go to home screen! + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 20, + }, + link: { + marginTop: 15, + paddingVertical: 15, + }, +}); diff --git a/packages/expo-template/app/_layout.tsx b/packages/expo-template/app/_layout.tsx new file mode 100644 index 0000000..6109bcc --- /dev/null +++ b/packages/expo-template/app/_layout.tsx @@ -0,0 +1,43 @@ +import { + DarkTheme, + DefaultTheme, + ThemeProvider, +} from '@react-navigation/native'; +import { useFonts } from 'expo-font'; +import { Stack } from 'expo-router'; +import * as SplashScreen from 'expo-splash-screen'; +import { StatusBar } from 'expo-status-bar'; +import { useEffect } from 'react'; +import 'react-native-reanimated'; + +import { useColorScheme } from '@/hooks/useColorScheme'; + +// Prevent the splash screen from auto-hiding before asset loading is complete. +SplashScreen.preventAutoHideAsync(); + +export default function RootLayout() { + const colorScheme = useColorScheme(); + const [loaded] = useFonts({ + SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), + }); + + useEffect(() => { + if (loaded) { + SplashScreen.hideAsync(); + } + }, [loaded]); + + if (!loaded) { + return null; + } + + return ( + + + + + + + + ); +} diff --git a/packages/expo-template/assets/fonts/SpaceMono-Regular.ttf b/packages/expo-template/assets/fonts/SpaceMono-Regular.ttf new file mode 100755 index 0000000..28d7ff7 Binary files /dev/null and b/packages/expo-template/assets/fonts/SpaceMono-Regular.ttf differ diff --git a/packages/expo-template/assets/images/adaptive-icon.png b/packages/expo-template/assets/images/adaptive-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/packages/expo-template/assets/images/adaptive-icon.png differ diff --git a/packages/expo-template/assets/images/favicon.png b/packages/expo-template/assets/images/favicon.png new file mode 100644 index 0000000..e75f697 Binary files /dev/null and b/packages/expo-template/assets/images/favicon.png differ diff --git a/packages/expo-template/assets/images/icon.png b/packages/expo-template/assets/images/icon.png new file mode 100644 index 0000000..a0b1526 Binary files /dev/null and b/packages/expo-template/assets/images/icon.png differ diff --git a/packages/expo-template/assets/images/partial-react-logo.png b/packages/expo-template/assets/images/partial-react-logo.png new file mode 100644 index 0000000..66fd957 Binary files /dev/null and b/packages/expo-template/assets/images/partial-react-logo.png differ diff --git a/packages/expo-template/assets/images/react-logo.png b/packages/expo-template/assets/images/react-logo.png new file mode 100644 index 0000000..9d72a9f Binary files /dev/null and b/packages/expo-template/assets/images/react-logo.png differ diff --git a/packages/expo-template/assets/images/react-logo@2x.png b/packages/expo-template/assets/images/react-logo@2x.png new file mode 100644 index 0000000..2229b13 Binary files /dev/null and b/packages/expo-template/assets/images/react-logo@2x.png differ diff --git a/packages/expo-template/assets/images/react-logo@3x.png b/packages/expo-template/assets/images/react-logo@3x.png new file mode 100644 index 0000000..a99b203 Binary files /dev/null and b/packages/expo-template/assets/images/react-logo@3x.png differ diff --git a/packages/expo-template/assets/images/splash-icon.png b/packages/expo-template/assets/images/splash-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/packages/expo-template/assets/images/splash-icon.png differ diff --git a/packages/expo-template/components/Collapsible.tsx b/packages/expo-template/components/Collapsible.tsx new file mode 100644 index 0000000..652afd3 --- /dev/null +++ b/packages/expo-template/components/Collapsible.tsx @@ -0,0 +1,49 @@ +import { PropsWithChildren, useState } from 'react'; +import { StyleSheet, TouchableOpacity } from 'react-native'; + +import { ThemedText } from '@/components/ThemedText'; +import { ThemedView } from '@/components/ThemedView'; +import { IconSymbol } from '@/components/ui/IconSymbol'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +export function Collapsible({ + children, + title, +}: PropsWithChildren & { title: string }) { + const [isOpen, setIsOpen] = useState(false); + const theme = useColorScheme() ?? 'light'; + + return ( + + setIsOpen((value) => !value)} + activeOpacity={0.8} + > + + + {title} + + {isOpen && {children}} + + ); +} + +const styles = StyleSheet.create({ + heading: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + content: { + marginTop: 6, + marginLeft: 24, + }, +}); diff --git a/packages/expo-template/components/ExternalLink.tsx b/packages/expo-template/components/ExternalLink.tsx new file mode 100644 index 0000000..8f05675 --- /dev/null +++ b/packages/expo-template/components/ExternalLink.tsx @@ -0,0 +1,24 @@ +import { Link } from 'expo-router'; +import { openBrowserAsync } from 'expo-web-browser'; +import { type ComponentProps } from 'react'; +import { Platform } from 'react-native'; + +type Props = Omit, 'href'> & { href: string }; + +export function ExternalLink({ href, ...rest }: Props) { + return ( + { + if (Platform.OS !== 'web') { + // Prevent the default behavior of linking to the default browser on native. + event.preventDefault(); + // Open the link in an in-app browser. + await openBrowserAsync(href); + } + }} + /> + ); +} diff --git a/packages/expo-template/components/HelloWave.tsx b/packages/expo-template/components/HelloWave.tsx new file mode 100644 index 0000000..05264ff --- /dev/null +++ b/packages/expo-template/components/HelloWave.tsx @@ -0,0 +1,43 @@ +import { useEffect } from 'react'; +import { StyleSheet } from 'react-native'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, + withRepeat, + withSequence, +} from 'react-native-reanimated'; + +import { ThemedText } from '@/components/ThemedText'; + +export function HelloWave() { + const rotationAnimation = useSharedValue(0); + + useEffect(() => { + rotationAnimation.value = withRepeat( + withSequence( + withTiming(25, { duration: 150 }), + withTiming(0, { duration: 150 }) + ), + 4 // Run the animation 4 times + ); + }, []); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ rotate: `${rotationAnimation.value}deg` }], + })); + + return ( + + šŸ‘‹ + + ); +} + +const styles = StyleSheet.create({ + text: { + fontSize: 28, + lineHeight: 32, + marginTop: -6, + }, +}); diff --git a/packages/expo-template/components/ParallaxScrollView.tsx b/packages/expo-template/components/ParallaxScrollView.tsx new file mode 100644 index 0000000..7b947ae --- /dev/null +++ b/packages/expo-template/components/ParallaxScrollView.tsx @@ -0,0 +1,85 @@ +import type { PropsWithChildren, ReactElement } from 'react'; +import { StyleSheet } from 'react-native'; +import Animated, { + interpolate, + useAnimatedRef, + useAnimatedStyle, + useScrollViewOffset, +} from 'react-native-reanimated'; + +import { ThemedView } from '@/components/ThemedView'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +const HEADER_HEIGHT = 250; + +type Props = PropsWithChildren<{ + headerImage: ReactElement; + headerBackgroundColor: { dark: string; light: string }; +}>; + +export default function ParallaxScrollView({ + children, + headerImage, + headerBackgroundColor, +}: Props) { + const colorScheme = useColorScheme() ?? 'light'; + const scrollRef = useAnimatedRef(); + const scrollOffset = useScrollViewOffset(scrollRef); + const headerAnimatedStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateY: interpolate( + scrollOffset.value, + [-HEADER_HEIGHT, 0, HEADER_HEIGHT], + [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75] + ), + }, + { + scale: interpolate( + scrollOffset.value, + [-HEADER_HEIGHT, 0, HEADER_HEIGHT], + [2, 1, 1] + ), + }, + ], + }; + }); + + return ( + + + + {headerImage} + + {children} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + height: HEADER_HEIGHT, + overflow: 'hidden', + }, + content: { + flex: 1, + padding: 32, + gap: 16, + overflow: 'hidden', + }, +}); diff --git a/packages/expo-template/components/ThemedText.tsx b/packages/expo-template/components/ThemedText.tsx new file mode 100644 index 0000000..c0e1a78 --- /dev/null +++ b/packages/expo-template/components/ThemedText.tsx @@ -0,0 +1,60 @@ +import { Text, type TextProps, StyleSheet } from 'react-native'; + +import { useThemeColor } from '@/hooks/useThemeColor'; + +export type ThemedTextProps = TextProps & { + lightColor?: string; + darkColor?: string; + type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'; +}; + +export function ThemedText({ + style, + lightColor, + darkColor, + type = 'default', + ...rest +}: ThemedTextProps) { + const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); + + return ( + + ); +} + +const styles = StyleSheet.create({ + default: { + fontSize: 16, + lineHeight: 24, + }, + defaultSemiBold: { + fontSize: 16, + lineHeight: 24, + fontWeight: '600', + }, + title: { + fontSize: 32, + fontWeight: 'bold', + lineHeight: 32, + }, + subtitle: { + fontSize: 20, + fontWeight: 'bold', + }, + link: { + lineHeight: 30, + fontSize: 16, + color: '#0a7ea4', + }, +}); diff --git a/packages/expo-template/components/ThemedView.tsx b/packages/expo-template/components/ThemedView.tsx new file mode 100644 index 0000000..af42a9f --- /dev/null +++ b/packages/expo-template/components/ThemedView.tsx @@ -0,0 +1,22 @@ +import { View, type ViewProps } from 'react-native'; + +import { useThemeColor } from '@/hooks/useThemeColor'; + +export type ThemedViewProps = ViewProps & { + lightColor?: string; + darkColor?: string; +}; + +export function ThemedView({ + style, + lightColor, + darkColor, + ...otherProps +}: ThemedViewProps) { + const backgroundColor = useThemeColor( + { light: lightColor, dark: darkColor }, + 'background' + ); + + return ; +} diff --git a/packages/expo-template/components/__tests__/ThemedText-test.tsx b/packages/expo-template/components/__tests__/ThemedText-test.tsx new file mode 100644 index 0000000..1ac3225 --- /dev/null +++ b/packages/expo-template/components/__tests__/ThemedText-test.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import renderer from 'react-test-renderer'; + +import { ThemedText } from '../ThemedText'; + +it(`renders correctly`, () => { + const tree = renderer.create(Snapshot test!).toJSON(); + + expect(tree).toMatchSnapshot(); +}); diff --git a/packages/expo-template/components/__tests__/__snapshots__/ThemedText-test.tsx.snap b/packages/expo-template/components/__tests__/__snapshots__/ThemedText-test.tsx.snap new file mode 100644 index 0000000..b68e53e --- /dev/null +++ b/packages/expo-template/components/__tests__/__snapshots__/ThemedText-test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + Snapshot test! + +`; diff --git a/packages/expo-template/components/ui/IconSymbol.ios.tsx b/packages/expo-template/components/ui/IconSymbol.ios.tsx new file mode 100644 index 0000000..9177f4d --- /dev/null +++ b/packages/expo-template/components/ui/IconSymbol.ios.tsx @@ -0,0 +1,32 @@ +import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols'; +import { StyleProp, ViewStyle } from 'react-native'; + +export function IconSymbol({ + name, + size = 24, + color, + style, + weight = 'regular', +}: { + name: SymbolViewProps['name']; + size?: number; + color: string; + style?: StyleProp; + weight?: SymbolWeight; +}) { + return ( + + ); +} diff --git a/packages/expo-template/components/ui/IconSymbol.tsx b/packages/expo-template/components/ui/IconSymbol.tsx new file mode 100644 index 0000000..9f6bf48 --- /dev/null +++ b/packages/expo-template/components/ui/IconSymbol.tsx @@ -0,0 +1,50 @@ +// This file is a fallback for using MaterialIcons on Android and web. + +import MaterialIcons from '@expo/vector-icons/MaterialIcons'; +import { SymbolWeight } from 'expo-symbols'; +import React from 'react'; +import { OpaqueColorValue, StyleProp, ViewStyle } from 'react-native'; + +// Add your SFSymbol to MaterialIcons mappings here. +const MAPPING = { + // See MaterialIcons here: https://icons.expo.fyi + // See SF Symbols in the SF Symbols app on Mac. + 'house.fill': 'home', + 'paperplane.fill': 'send', + 'chevron.left.forwardslash.chevron.right': 'code', + 'chevron.right': 'chevron-right', +} as Partial< + Record< + import('expo-symbols').SymbolViewProps['name'], + React.ComponentProps['name'] + > +>; + +export type IconSymbolName = keyof typeof MAPPING; + +/** + * An icon component that uses native SFSymbols on iOS, and MaterialIcons on Android and web. This ensures a consistent look across platforms, and optimal resource usage. + * + * Icon `name`s are based on SFSymbols and require manual mapping to MaterialIcons. + */ +export function IconSymbol({ + name, + size = 24, + color, + style, +}: { + name: IconSymbolName; + size?: number; + color: string | OpaqueColorValue; + style?: StyleProp; + weight?: SymbolWeight; +}) { + return ( + + ); +} diff --git a/packages/expo-template/constants/Colors.ts b/packages/expo-template/constants/Colors.ts new file mode 100644 index 0000000..14e6784 --- /dev/null +++ b/packages/expo-template/constants/Colors.ts @@ -0,0 +1,26 @@ +/** + * Below are the colors that are used in the app. The colors are defined in the light and dark mode. + * 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. + */ + +const tintColorLight = '#0a7ea4'; +const tintColorDark = '#fff'; + +export const Colors = { + light: { + text: '#11181C', + background: '#fff', + tint: tintColorLight, + icon: '#687076', + tabIconDefault: '#687076', + tabIconSelected: tintColorLight, + }, + dark: { + text: '#ECEDEE', + background: '#151718', + tint: tintColorDark, + icon: '#9BA1A6', + tabIconDefault: '#9BA1A6', + tabIconSelected: tintColorDark, + }, +}; diff --git a/packages/expo-template/gitignore b/packages/expo-template/gitignore new file mode 100644 index 0000000..c9d575d --- /dev/null +++ b/packages/expo-template/gitignore @@ -0,0 +1,38 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +app-example diff --git a/packages/expo-template/hooks/useColorScheme.ts b/packages/expo-template/hooks/useColorScheme.ts new file mode 100644 index 0000000..17e3c63 --- /dev/null +++ b/packages/expo-template/hooks/useColorScheme.ts @@ -0,0 +1 @@ +export { useColorScheme } from 'react-native'; diff --git a/packages/expo-template/hooks/useColorScheme.web.ts b/packages/expo-template/hooks/useColorScheme.web.ts new file mode 100644 index 0000000..7eb1c1b --- /dev/null +++ b/packages/expo-template/hooks/useColorScheme.web.ts @@ -0,0 +1,21 @@ +import { useEffect, useState } from 'react'; +import { useColorScheme as useRNColorScheme } from 'react-native'; + +/** + * To support static rendering, this value needs to be re-calculated on the client side for web + */ +export function useColorScheme() { + const [hasHydrated, setHasHydrated] = useState(false); + + useEffect(() => { + setHasHydrated(true); + }, []); + + const colorScheme = useRNColorScheme(); + + if (hasHydrated) { + return colorScheme; + } + + return 'light'; +} diff --git a/packages/expo-template/hooks/useThemeColor.ts b/packages/expo-template/hooks/useThemeColor.ts new file mode 100644 index 0000000..0608e73 --- /dev/null +++ b/packages/expo-template/hooks/useThemeColor.ts @@ -0,0 +1,21 @@ +/** + * Learn more about light and dark modes: + * https://docs.expo.dev/guides/color-schemes/ + */ + +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +export function useThemeColor( + props: { light?: string; dark?: string }, + colorName: keyof typeof Colors.light & keyof typeof Colors.dark +) { + const theme = useColorScheme() ?? 'light'; + const colorFromProps = props[theme]; + + if (colorFromProps) { + return colorFromProps; + } else { + return Colors[theme][colorName]; + } +} diff --git a/packages/expo-template/package.json b/packages/expo-template/package.json new file mode 100644 index 0000000..8a2f409 --- /dev/null +++ b/packages/expo-template/package.json @@ -0,0 +1,54 @@ +{ + "name": "@bottom-tabs/expo-template", + "main": "expo-router/entry", + "version": "0.0.1", + "scripts": { + "start": "expo start", + "reset-project": "node ./scripts/reset-project.js", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web", + "test": "jest --watchAll", + "lint": "expo lint" + }, + "jest": { + "preset": "jest-expo" + }, + "dependencies": { + "@expo/vector-icons": "^14.0.2", + "react-native-bottom-tabs": "0.8.1", + "@bottom-tabs/react-navigation": "0.8.1", + "@react-navigation/native": "^7.0.14", + "expo": "~52.0.11", + "expo-blur": "~14.0.1", + "expo-constants": "~17.0.3", + "expo-font": "~13.0.1", + "expo-haptics": "~14.0.0", + "expo-linking": "~7.0.3", + "expo-router": "~4.0.9", + "expo-splash-screen": "~0.29.13", + "expo-status-bar": "~2.0.0", + "expo-symbols": "~0.2.0", + "expo-system-ui": "~4.0.4", + "expo-web-browser": "~14.0.1", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-native": "0.76.3", + "react-native-gesture-handler": "~2.20.2", + "react-native-reanimated": "~3.16.1", + "react-native-safe-area-context": "4.12.0", + "react-native-screens": "~4.4.0", + "react-native-web": "~0.19.13", + "react-native-webview": "13.12.2" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@types/jest": "^29.5.12", + "@types/react": "~18.3.12", + "@types/react-test-renderer": "^18.3.0", + "jest": "^29.2.1", + "jest-expo": "~52.0.2", + "react-test-renderer": "18.3.1", + "typescript": "^5.3.3" + } +} diff --git a/packages/expo-template/scripts/reset-project.js b/packages/expo-template/scripts/reset-project.js new file mode 100755 index 0000000..5f81463 --- /dev/null +++ b/packages/expo-template/scripts/reset-project.js @@ -0,0 +1,84 @@ +#!/usr/bin/env node + +/** + * This script is used to reset the project to a blank state. + * It moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file. + * You can remove the `reset-project` script from package.json and safely delete this file after running it. + */ + +const fs = require("fs"); +const path = require("path"); + +const root = process.cwd(); +const oldDirs = ["app", "components", "hooks", "constants", "scripts"]; +const newDir = "app-example"; +const newAppDir = "app"; +const newDirPath = path.join(root, newDir); + +const indexContent = `import { Text, View } from "react-native"; + +export default function Index() { + return ( + + Edit app/index.tsx to edit this screen. + + ); +} +`; + +const layoutContent = `import { Stack } from "expo-router"; + +export default function RootLayout() { + return ; +} +`; + +const moveDirectories = async () => { + try { + // Create the app-example directory + await fs.promises.mkdir(newDirPath, { recursive: true }); + console.log(`šŸ“ /${newDir} directory created.`); + + // Move old directories to new app-example directory + for (const dir of oldDirs) { + const oldDirPath = path.join(root, dir); + const newDirPath = path.join(root, newDir, dir); + if (fs.existsSync(oldDirPath)) { + await fs.promises.rename(oldDirPath, newDirPath); + console.log(`āž”ļø /${dir} moved to /${newDir}/${dir}.`); + } else { + console.log(`āž”ļø /${dir} does not exist, skipping.`); + } + } + + // Create new /app directory + const newAppDirPath = path.join(root, newAppDir); + await fs.promises.mkdir(newAppDirPath, { recursive: true }); + console.log("\nšŸ“ New /app directory created."); + + // Create index.tsx + const indexPath = path.join(newAppDirPath, "index.tsx"); + await fs.promises.writeFile(indexPath, indexContent); + console.log("šŸ“„ app/index.tsx created."); + + // Create _layout.tsx + const layoutPath = path.join(newAppDirPath, "_layout.tsx"); + await fs.promises.writeFile(layoutPath, layoutContent); + console.log("šŸ“„ app/_layout.tsx created."); + + console.log("\nāœ… Project reset complete. Next steps:"); + console.log( + "1. Run `npx expo start` to start a development server.\n2. Edit app/index.tsx to edit the main screen.\n3. Delete the /app-example directory when you're done referencing it." + ); + } catch (error) { + console.error(`Error during script execution: ${error}`); + } +}; + +moveDirectories(); diff --git a/packages/expo-template/tsconfig.json b/packages/expo-template/tsconfig.json new file mode 100644 index 0000000..909e901 --- /dev/null +++ b/packages/expo-template/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ".expo/types/**/*.ts", + "expo-env.d.ts" + ] +}