Initial commit
Generated by create-expo-app 3.4.2.
							
								
								
									
										39
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,39 @@ | |||||||
|  | # 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 | ||||||
|  | .kotlin/ | ||||||
|  | *.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 | ||||||
							
								
								
									
										7
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | |||||||
|  | { | ||||||
|  |   "editor.codeActionsOnSave": { | ||||||
|  |     "source.fixAll": "explicit", | ||||||
|  |     "source.organizeImports": "explicit", | ||||||
|  |     "source.sortMembers": "explicit" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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. | ||||||
							
								
								
									
										42
									
								
								app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | |||||||
|  | { | ||||||
|  |   "expo": { | ||||||
|  |     "name": "flexlark-template", | ||||||
|  |     "slug": "flexlark-template", | ||||||
|  |     "version": "1.0.0", | ||||||
|  |     "orientation": "portrait", | ||||||
|  |     "icon": "./assets/images/icon.png", | ||||||
|  |     "scheme": "flexlarktemplate", | ||||||
|  |     "userInterfaceStyle": "automatic", | ||||||
|  |     "newArchEnabled": true, | ||||||
|  |     "ios": { | ||||||
|  |       "supportsTablet": true | ||||||
|  |     }, | ||||||
|  |     "android": { | ||||||
|  |       "adaptiveIcon": { | ||||||
|  |         "foregroundImage": "./assets/images/adaptive-icon.png", | ||||||
|  |         "backgroundColor": "#ffffff" | ||||||
|  |       }, | ||||||
|  |       "edgeToEdgeEnabled": true | ||||||
|  |     }, | ||||||
|  |     "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 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								app/(tabs)/_layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,45 @@ | |||||||
|  | import { Tabs } from 'expo-router'; | ||||||
|  | import React from 'react'; | ||||||
|  | import { Platform } from 'react-native'; | ||||||
|  |  | ||||||
|  | import { HapticTab } from '@/components/HapticTab'; | ||||||
|  | import { IconSymbol } from '@/components/ui/IconSymbol'; | ||||||
|  | import TabBarBackground from '@/components/ui/TabBarBackground'; | ||||||
|  | import { Colors } from '@/constants/Colors'; | ||||||
|  | import { useColorScheme } from '@/hooks/useColorScheme'; | ||||||
|  |  | ||||||
|  | export default function TabLayout() { | ||||||
|  |   const colorScheme = useColorScheme(); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Tabs | ||||||
|  |       screenOptions={{ | ||||||
|  |         tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, | ||||||
|  |         headerShown: false, | ||||||
|  |         tabBarButton: HapticTab, | ||||||
|  |         tabBarBackground: TabBarBackground, | ||||||
|  |         tabBarStyle: Platform.select({ | ||||||
|  |           ios: { | ||||||
|  |             // Use a transparent background on iOS to show the blur effect | ||||||
|  |             position: 'absolute', | ||||||
|  |           }, | ||||||
|  |           default: {}, | ||||||
|  |         }), | ||||||
|  |       }}> | ||||||
|  |       <Tabs.Screen | ||||||
|  |         name="index" | ||||||
|  |         options={{ | ||||||
|  |           title: 'Home', | ||||||
|  |           tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />, | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|  |       <Tabs.Screen | ||||||
|  |         name="explore" | ||||||
|  |         options={{ | ||||||
|  |           title: 'Explore', | ||||||
|  |           tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />, | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|  |     </Tabs> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										110
									
								
								app/(tabs)/explore.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,110 @@ | |||||||
|  | import { Image } from 'expo-image'; | ||||||
|  | import { Platform, StyleSheet } 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 ( | ||||||
|  |     <ParallaxScrollView | ||||||
|  |       headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }} | ||||||
|  |       headerImage={ | ||||||
|  |         <IconSymbol | ||||||
|  |           size={310} | ||||||
|  |           color="#808080" | ||||||
|  |           name="chevron.left.forwardslash.chevron.right" | ||||||
|  |           style={styles.headerImage} | ||||||
|  |         /> | ||||||
|  |       }> | ||||||
|  |       <ThemedView style={styles.titleContainer}> | ||||||
|  |         <ThemedText type="title">Explore</ThemedText> | ||||||
|  |       </ThemedView> | ||||||
|  |       <ThemedText>This app includes example code to help you get started.</ThemedText> | ||||||
|  |       <Collapsible title="File-based routing"> | ||||||
|  |         <ThemedText> | ||||||
|  |           This app has two screens:{' '} | ||||||
|  |           <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '} | ||||||
|  |           <ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText> | ||||||
|  |         </ThemedText> | ||||||
|  |         <ThemedText> | ||||||
|  |           The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '} | ||||||
|  |           sets up the tab navigator. | ||||||
|  |         </ThemedText> | ||||||
|  |         <ExternalLink href="https://docs.expo.dev/router/introduction"> | ||||||
|  |           <ThemedText type="link">Learn more</ThemedText> | ||||||
|  |         </ExternalLink> | ||||||
|  |       </Collapsible> | ||||||
|  |       <Collapsible title="Android, iOS, and web support"> | ||||||
|  |         <ThemedText> | ||||||
|  |           You can open this project on Android, iOS, and the web. To open the web version, press{' '} | ||||||
|  |           <ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project. | ||||||
|  |         </ThemedText> | ||||||
|  |       </Collapsible> | ||||||
|  |       <Collapsible title="Images"> | ||||||
|  |         <ThemedText> | ||||||
|  |           For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '} | ||||||
|  |           <ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for | ||||||
|  |           different screen densities | ||||||
|  |         </ThemedText> | ||||||
|  |         <Image source={require('@/assets/images/react-logo.png')} style={{ alignSelf: 'center' }} /> | ||||||
|  |         <ExternalLink href="https://reactnative.dev/docs/images"> | ||||||
|  |           <ThemedText type="link">Learn more</ThemedText> | ||||||
|  |         </ExternalLink> | ||||||
|  |       </Collapsible> | ||||||
|  |       <Collapsible title="Custom fonts"> | ||||||
|  |         <ThemedText> | ||||||
|  |           Open <ThemedText type="defaultSemiBold">app/_layout.tsx</ThemedText> to see how to load{' '} | ||||||
|  |           <ThemedText style={{ fontFamily: 'SpaceMono' }}> | ||||||
|  |             custom fonts such as this one. | ||||||
|  |           </ThemedText> | ||||||
|  |         </ThemedText> | ||||||
|  |         <ExternalLink href="https://docs.expo.dev/versions/latest/sdk/font"> | ||||||
|  |           <ThemedText type="link">Learn more</ThemedText> | ||||||
|  |         </ExternalLink> | ||||||
|  |       </Collapsible> | ||||||
|  |       <Collapsible title="Light and dark mode components"> | ||||||
|  |         <ThemedText> | ||||||
|  |           This template has light and dark mode support. The{' '} | ||||||
|  |           <ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect | ||||||
|  |           what the user's current color scheme is, and so you can adjust UI colors accordingly. | ||||||
|  |         </ThemedText> | ||||||
|  |         <ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/"> | ||||||
|  |           <ThemedText type="link">Learn more</ThemedText> | ||||||
|  |         </ExternalLink> | ||||||
|  |       </Collapsible> | ||||||
|  |       <Collapsible title="Animations"> | ||||||
|  |         <ThemedText> | ||||||
|  |           This template includes an example of an animated component. The{' '} | ||||||
|  |           <ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses | ||||||
|  |           the powerful <ThemedText type="defaultSemiBold">react-native-reanimated</ThemedText>{' '} | ||||||
|  |           library to create a waving hand animation. | ||||||
|  |         </ThemedText> | ||||||
|  |         {Platform.select({ | ||||||
|  |           ios: ( | ||||||
|  |             <ThemedText> | ||||||
|  |               The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '} | ||||||
|  |               component provides a parallax effect for the header image. | ||||||
|  |             </ThemedText> | ||||||
|  |           ), | ||||||
|  |         })} | ||||||
|  |       </Collapsible> | ||||||
|  |     </ParallaxScrollView> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const styles = StyleSheet.create({ | ||||||
|  |   headerImage: { | ||||||
|  |     color: '#808080', | ||||||
|  |     bottom: -90, | ||||||
|  |     left: -35, | ||||||
|  |     position: 'absolute', | ||||||
|  |   }, | ||||||
|  |   titleContainer: { | ||||||
|  |     flexDirection: 'row', | ||||||
|  |     gap: 8, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
							
								
								
									
										75
									
								
								app/(tabs)/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,75 @@ | |||||||
|  | import { Image } from 'expo-image'; | ||||||
|  | import { Platform, StyleSheet } 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 ( | ||||||
|  |     <ParallaxScrollView | ||||||
|  |       headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }} | ||||||
|  |       headerImage={ | ||||||
|  |         <Image | ||||||
|  |           source={require('@/assets/images/partial-react-logo.png')} | ||||||
|  |           style={styles.reactLogo} | ||||||
|  |         /> | ||||||
|  |       }> | ||||||
|  |       <ThemedView style={styles.titleContainer}> | ||||||
|  |         <ThemedText type="title">Welcome!</ThemedText> | ||||||
|  |         <HelloWave /> | ||||||
|  |       </ThemedView> | ||||||
|  |       <ThemedView style={styles.stepContainer}> | ||||||
|  |         <ThemedText type="subtitle">Step 1: Try it</ThemedText> | ||||||
|  |         <ThemedText> | ||||||
|  |           Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes. | ||||||
|  |           Press{' '} | ||||||
|  |           <ThemedText type="defaultSemiBold"> | ||||||
|  |             {Platform.select({ | ||||||
|  |               ios: 'cmd + d', | ||||||
|  |               android: 'cmd + m', | ||||||
|  |               web: 'F12', | ||||||
|  |             })} | ||||||
|  |           </ThemedText>{' '} | ||||||
|  |           to open developer tools. | ||||||
|  |         </ThemedText> | ||||||
|  |       </ThemedView> | ||||||
|  |       <ThemedView style={styles.stepContainer}> | ||||||
|  |         <ThemedText type="subtitle">Step 2: Explore</ThemedText> | ||||||
|  |         <ThemedText> | ||||||
|  |           {`Tap the Explore tab to learn more about what's included in this starter app.`} | ||||||
|  |         </ThemedText> | ||||||
|  |       </ThemedView> | ||||||
|  |       <ThemedView style={styles.stepContainer}> | ||||||
|  |         <ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText> | ||||||
|  |         <ThemedText> | ||||||
|  |           {`When you're ready, run `} | ||||||
|  |           <ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '} | ||||||
|  |           <ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '} | ||||||
|  |           <ThemedText type="defaultSemiBold">app</ThemedText> to{' '} | ||||||
|  |           <ThemedText type="defaultSemiBold">app-example</ThemedText>. | ||||||
|  |         </ThemedText> | ||||||
|  |       </ThemedView> | ||||||
|  |     </ParallaxScrollView> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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', | ||||||
|  |   }, | ||||||
|  | }); | ||||||
							
								
								
									
										32
									
								
								app/+not-found.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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 ( | ||||||
|  |     <> | ||||||
|  |       <Stack.Screen options={{ title: 'Oops!' }} /> | ||||||
|  |       <ThemedView style={styles.container}> | ||||||
|  |         <ThemedText type="title">This screen does not exist.</ThemedText> | ||||||
|  |         <Link href="/" style={styles.link}> | ||||||
|  |           <ThemedText type="link">Go to home screen!</ThemedText> | ||||||
|  |         </Link> | ||||||
|  |       </ThemedView> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const styles = StyleSheet.create({ | ||||||
|  |   container: { | ||||||
|  |     flex: 1, | ||||||
|  |     alignItems: 'center', | ||||||
|  |     justifyContent: 'center', | ||||||
|  |     padding: 20, | ||||||
|  |   }, | ||||||
|  |   link: { | ||||||
|  |     marginTop: 15, | ||||||
|  |     paddingVertical: 15, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
							
								
								
									
										29
									
								
								app/_layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,29 @@ | |||||||
|  | import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; | ||||||
|  | import { useFonts } from 'expo-font'; | ||||||
|  | import { Stack } from 'expo-router'; | ||||||
|  | import { StatusBar } from 'expo-status-bar'; | ||||||
|  | import 'react-native-reanimated'; | ||||||
|  |  | ||||||
|  | import { useColorScheme } from '@/hooks/useColorScheme'; | ||||||
|  |  | ||||||
|  | export default function RootLayout() { | ||||||
|  |   const colorScheme = useColorScheme(); | ||||||
|  |   const [loaded] = useFonts({ | ||||||
|  |     SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   if (!loaded) { | ||||||
|  |     // Async font loading only occurs in development. | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> | ||||||
|  |       <Stack> | ||||||
|  |         <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> | ||||||
|  |         <Stack.Screen name="+not-found" /> | ||||||
|  |       </Stack> | ||||||
|  |       <StatusBar style="auto" /> | ||||||
|  |     </ThemeProvider> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								assets/fonts/SpaceMono-Regular.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								assets/images/adaptive-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 17 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/partial-react-logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/react-logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/react-logo@2x.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/react-logo@3x.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 21 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/splash-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 17 KiB | 
							
								
								
									
										45
									
								
								components/Collapsible.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,45 @@ | |||||||
|  | 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 ( | ||||||
|  |     <ThemedView> | ||||||
|  |       <TouchableOpacity | ||||||
|  |         style={styles.heading} | ||||||
|  |         onPress={() => setIsOpen((value) => !value)} | ||||||
|  |         activeOpacity={0.8}> | ||||||
|  |         <IconSymbol | ||||||
|  |           name="chevron.right" | ||||||
|  |           size={18} | ||||||
|  |           weight="medium" | ||||||
|  |           color={theme === 'light' ? Colors.light.icon : Colors.dark.icon} | ||||||
|  |           style={{ transform: [{ rotate: isOpen ? '90deg' : '0deg' }] }} | ||||||
|  |         /> | ||||||
|  |  | ||||||
|  |         <ThemedText type="defaultSemiBold">{title}</ThemedText> | ||||||
|  |       </TouchableOpacity> | ||||||
|  |       {isOpen && <ThemedView style={styles.content}>{children}</ThemedView>} | ||||||
|  |     </ThemedView> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const styles = StyleSheet.create({ | ||||||
|  |   heading: { | ||||||
|  |     flexDirection: 'row', | ||||||
|  |     alignItems: 'center', | ||||||
|  |     gap: 6, | ||||||
|  |   }, | ||||||
|  |   content: { | ||||||
|  |     marginTop: 6, | ||||||
|  |     marginLeft: 24, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
							
								
								
									
										24
									
								
								components/ExternalLink.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | |||||||
|  | import { Href, Link } from 'expo-router'; | ||||||
|  | import { openBrowserAsync } from 'expo-web-browser'; | ||||||
|  | import { type ComponentProps } from 'react'; | ||||||
|  | import { Platform } from 'react-native'; | ||||||
|  |  | ||||||
|  | type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: Href & string }; | ||||||
|  |  | ||||||
|  | export function ExternalLink({ href, ...rest }: Props) { | ||||||
|  |   return ( | ||||||
|  |     <Link | ||||||
|  |       target="_blank" | ||||||
|  |       {...rest} | ||||||
|  |       href={href} | ||||||
|  |       onPress={async (event) => { | ||||||
|  |         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); | ||||||
|  |         } | ||||||
|  |       }} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								components/HapticTab.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | |||||||
|  | import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs'; | ||||||
|  | import { PlatformPressable } from '@react-navigation/elements'; | ||||||
|  | import * as Haptics from 'expo-haptics'; | ||||||
|  |  | ||||||
|  | export function HapticTab(props: BottomTabBarButtonProps) { | ||||||
|  |   return ( | ||||||
|  |     <PlatformPressable | ||||||
|  |       {...props} | ||||||
|  |       onPressIn={(ev) => { | ||||||
|  |         if (process.env.EXPO_OS === 'ios') { | ||||||
|  |           // Add a soft haptic feedback when pressing down on the tabs. | ||||||
|  |           Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); | ||||||
|  |         } | ||||||
|  |         props.onPressIn?.(ev); | ||||||
|  |       }} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								components/HelloWave.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,40 @@ | |||||||
|  | import { useEffect } from 'react'; | ||||||
|  | import { StyleSheet } from 'react-native'; | ||||||
|  | import Animated, { | ||||||
|  |   useAnimatedStyle, | ||||||
|  |   useSharedValue, | ||||||
|  |   withRepeat, | ||||||
|  |   withSequence, | ||||||
|  |   withTiming, | ||||||
|  | } 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 | ||||||
|  |     ); | ||||||
|  |   }, [rotationAnimation]); | ||||||
|  |  | ||||||
|  |   const animatedStyle = useAnimatedStyle(() => ({ | ||||||
|  |     transform: [{ rotate: `${rotationAnimation.value}deg` }], | ||||||
|  |   })); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Animated.View style={animatedStyle}> | ||||||
|  |       <ThemedText style={styles.text}>👋</ThemedText> | ||||||
|  |     </Animated.View> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const styles = StyleSheet.create({ | ||||||
|  |   text: { | ||||||
|  |     fontSize: 28, | ||||||
|  |     lineHeight: 32, | ||||||
|  |     marginTop: -6, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
							
								
								
									
										82
									
								
								components/ParallaxScrollView.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,82 @@ | |||||||
|  | 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 { useBottomTabOverflow } from '@/components/ui/TabBarBackground'; | ||||||
|  | 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<Animated.ScrollView>(); | ||||||
|  |   const scrollOffset = useScrollViewOffset(scrollRef); | ||||||
|  |   const bottom = useBottomTabOverflow(); | ||||||
|  |   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 ( | ||||||
|  |     <ThemedView style={styles.container}> | ||||||
|  |       <Animated.ScrollView | ||||||
|  |         ref={scrollRef} | ||||||
|  |         scrollEventThrottle={16} | ||||||
|  |         scrollIndicatorInsets={{ bottom }} | ||||||
|  |         contentContainerStyle={{ paddingBottom: bottom }}> | ||||||
|  |         <Animated.View | ||||||
|  |           style={[ | ||||||
|  |             styles.header, | ||||||
|  |             { backgroundColor: headerBackgroundColor[colorScheme] }, | ||||||
|  |             headerAnimatedStyle, | ||||||
|  |           ]}> | ||||||
|  |           {headerImage} | ||||||
|  |         </Animated.View> | ||||||
|  |         <ThemedView style={styles.content}>{children}</ThemedView> | ||||||
|  |       </Animated.ScrollView> | ||||||
|  |     </ThemedView> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const styles = StyleSheet.create({ | ||||||
|  |   container: { | ||||||
|  |     flex: 1, | ||||||
|  |   }, | ||||||
|  |   header: { | ||||||
|  |     height: HEADER_HEIGHT, | ||||||
|  |     overflow: 'hidden', | ||||||
|  |   }, | ||||||
|  |   content: { | ||||||
|  |     flex: 1, | ||||||
|  |     padding: 32, | ||||||
|  |     gap: 16, | ||||||
|  |     overflow: 'hidden', | ||||||
|  |   }, | ||||||
|  | }); | ||||||
							
								
								
									
										60
									
								
								components/ThemedText.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,60 @@ | |||||||
|  | import { StyleSheet, Text, type TextProps } 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 ( | ||||||
|  |     <Text | ||||||
|  |       style={[ | ||||||
|  |         { color }, | ||||||
|  |         type === 'default' ? styles.default : undefined, | ||||||
|  |         type === 'title' ? styles.title : undefined, | ||||||
|  |         type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined, | ||||||
|  |         type === 'subtitle' ? styles.subtitle : undefined, | ||||||
|  |         type === 'link' ? styles.link : undefined, | ||||||
|  |         style, | ||||||
|  |       ]} | ||||||
|  |       {...rest} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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', | ||||||
|  |   }, | ||||||
|  | }); | ||||||
							
								
								
									
										14
									
								
								components/ThemedView.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | |||||||
|  | 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 <View style={[{ backgroundColor }, style]} {...otherProps} />; | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								components/ui/IconSymbol.ios.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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<ViewStyle>; | ||||||
|  |   weight?: SymbolWeight; | ||||||
|  | }) { | ||||||
|  |   return ( | ||||||
|  |     <SymbolView | ||||||
|  |       weight={weight} | ||||||
|  |       tintColor={color} | ||||||
|  |       resizeMode="scaleAspectFit" | ||||||
|  |       name={name} | ||||||
|  |       style={[ | ||||||
|  |         { | ||||||
|  |           width: size, | ||||||
|  |           height: size, | ||||||
|  |         }, | ||||||
|  |         style, | ||||||
|  |       ]} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										41
									
								
								components/ui/IconSymbol.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | |||||||
|  | // Fallback for using MaterialIcons on Android and web. | ||||||
|  |  | ||||||
|  | import MaterialIcons from '@expo/vector-icons/MaterialIcons'; | ||||||
|  | import { SymbolWeight, SymbolViewProps } from 'expo-symbols'; | ||||||
|  | import { ComponentProps } from 'react'; | ||||||
|  | import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native'; | ||||||
|  |  | ||||||
|  | type IconMapping = Record<SymbolViewProps['name'], ComponentProps<typeof MaterialIcons>['name']>; | ||||||
|  | type IconSymbolName = keyof typeof MAPPING; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Add your SF Symbols to Material Icons mappings here. | ||||||
|  |  * - see Material Icons in the [Icons Directory](https://icons.expo.fyi). | ||||||
|  |  * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app. | ||||||
|  |  */ | ||||||
|  | const MAPPING = { | ||||||
|  |   'house.fill': 'home', | ||||||
|  |   'paperplane.fill': 'send', | ||||||
|  |   'chevron.left.forwardslash.chevron.right': 'code', | ||||||
|  |   'chevron.right': 'chevron-right', | ||||||
|  | } as IconMapping; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web. | ||||||
|  |  * This ensures a consistent look across platforms, and optimal resource usage. | ||||||
|  |  * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons. | ||||||
|  |  */ | ||||||
|  | export function IconSymbol({ | ||||||
|  |   name, | ||||||
|  |   size = 24, | ||||||
|  |   color, | ||||||
|  |   style, | ||||||
|  | }: { | ||||||
|  |   name: IconSymbolName; | ||||||
|  |   size?: number; | ||||||
|  |   color: string | OpaqueColorValue; | ||||||
|  |   style?: StyleProp<TextStyle>; | ||||||
|  |   weight?: SymbolWeight; | ||||||
|  | }) { | ||||||
|  |   return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />; | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								components/ui/TabBarBackground.ios.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | |||||||
|  | import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; | ||||||
|  | import { BlurView } from 'expo-blur'; | ||||||
|  | import { StyleSheet } from 'react-native'; | ||||||
|  |  | ||||||
|  | export default function BlurTabBarBackground() { | ||||||
|  |   return ( | ||||||
|  |     <BlurView | ||||||
|  |       // System chrome material automatically adapts to the system's theme | ||||||
|  |       // and matches the native tab bar appearance on iOS. | ||||||
|  |       tint="systemChromeMaterial" | ||||||
|  |       intensity={100} | ||||||
|  |       style={StyleSheet.absoluteFill} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function useBottomTabOverflow() { | ||||||
|  |   return useBottomTabBarHeight(); | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								components/ui/TabBarBackground.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | // This is a shim for web and Android where the tab bar is generally opaque. | ||||||
|  | export default undefined; | ||||||
|  |  | ||||||
|  | export function useBottomTabOverflow() { | ||||||
|  |   return 0; | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								constants/Colors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
							
								
								
									
										10
									
								
								eslint.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | |||||||
|  | // https://docs.expo.dev/guides/using-eslint/ | ||||||
|  | const { defineConfig } = require('eslint/config'); | ||||||
|  | const expoConfig = require('eslint-config-expo/flat'); | ||||||
|  |  | ||||||
|  | module.exports = defineConfig([ | ||||||
|  |   expoConfig, | ||||||
|  |   { | ||||||
|  |     ignores: ['dist/*'], | ||||||
|  |   }, | ||||||
|  | ]); | ||||||
							
								
								
									
										1
									
								
								hooks/useColorScheme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | export { useColorScheme } from 'react-native'; | ||||||
							
								
								
									
										21
									
								
								hooks/useColorScheme.web.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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'; | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								hooks/useThemeColor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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]; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,49 @@ | |||||||
|  | { | ||||||
|  |   "name": "flexlark-template", | ||||||
|  |   "main": "expo-router/entry", | ||||||
|  |   "version": "1.0.0", | ||||||
|  |   "scripts": { | ||||||
|  |     "start": "expo start", | ||||||
|  |     "reset-project": "node ./scripts/reset-project.js", | ||||||
|  |     "android": "expo start --android", | ||||||
|  |     "ios": "expo start --ios", | ||||||
|  |     "web": "expo start --web", | ||||||
|  |     "lint": "expo lint" | ||||||
|  |   }, | ||||||
|  |   "dependencies": { | ||||||
|  |     "@expo/vector-icons": "^14.1.0", | ||||||
|  |     "@react-navigation/bottom-tabs": "^7.3.10", | ||||||
|  |     "@react-navigation/elements": "^2.3.8", | ||||||
|  |     "@react-navigation/native": "^7.1.6", | ||||||
|  |     "expo": "~53.0.20", | ||||||
|  |     "expo-blur": "~14.1.5", | ||||||
|  |     "expo-constants": "~17.1.7", | ||||||
|  |     "expo-font": "~13.3.2", | ||||||
|  |     "expo-haptics": "~14.1.4", | ||||||
|  |     "expo-image": "~2.4.0", | ||||||
|  |     "expo-linking": "~7.1.7", | ||||||
|  |     "expo-router": "~5.1.4", | ||||||
|  |     "expo-splash-screen": "~0.30.10", | ||||||
|  |     "expo-status-bar": "~2.2.3", | ||||||
|  |     "expo-symbols": "~0.4.5", | ||||||
|  |     "expo-system-ui": "~5.0.10", | ||||||
|  |     "expo-web-browser": "~14.2.0", | ||||||
|  |     "react": "19.0.0", | ||||||
|  |     "react-dom": "19.0.0", | ||||||
|  |     "react-native": "0.79.5", | ||||||
|  |     "react-native-gesture-handler": "~2.24.0", | ||||||
|  |     "react-native-reanimated": "~3.17.4", | ||||||
|  |     "react-native-safe-area-context": "5.4.0", | ||||||
|  |     "react-native-screens": "~4.11.1", | ||||||
|  |     "react-native-web": "~0.20.0", | ||||||
|  |     "react-native-webview": "13.13.5" | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "@babel/core": "^7.25.2", | ||||||
|  |     "@types/react": "~19.0.10", | ||||||
|  |     "typescript": "~5.8.3", | ||||||
|  |     "eslint": "^9.25.0", | ||||||
|  |     "eslint-config-expo": "~9.2.0" | ||||||
|  |   }, | ||||||
|  |   "private": true | ||||||
|  | } | ||||||
							
								
								
									
										8593
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1
									
								
								pnpm-workspace.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | nodeLinker: hoisted | ||||||
							
								
								
									
										112
									
								
								scripts/reset-project.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,112 @@ | |||||||
|  | #!/usr/bin/env node | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This script is used to reset the project to a blank state. | ||||||
|  |  * 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. | ||||||
|  |  * 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 readline = require("readline"); | ||||||
|  |  | ||||||
|  | const root = process.cwd(); | ||||||
|  | const oldDirs = ["app", "components", "hooks", "constants", "scripts"]; | ||||||
|  | const exampleDir = "app-example"; | ||||||
|  | const newAppDir = "app"; | ||||||
|  | const exampleDirPath = path.join(root, exampleDir); | ||||||
|  |  | ||||||
|  | const indexContent = `import { Text, View } from "react-native"; | ||||||
|  |  | ||||||
|  | export default function Index() { | ||||||
|  |   return ( | ||||||
|  |     <View | ||||||
|  |       style={{ | ||||||
|  |         flex: 1, | ||||||
|  |         justifyContent: "center", | ||||||
|  |         alignItems: "center", | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       <Text>Edit app/index.tsx to edit this screen.</Text> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | `; | ||||||
|  |  | ||||||
|  | const layoutContent = `import { Stack } from "expo-router"; | ||||||
|  |  | ||||||
|  | export default function RootLayout() { | ||||||
|  |   return <Stack />; | ||||||
|  | } | ||||||
|  | `; | ||||||
|  |  | ||||||
|  | const rl = readline.createInterface({ | ||||||
|  |   input: process.stdin, | ||||||
|  |   output: process.stdout, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const moveDirectories = async (userInput) => { | ||||||
|  |   try { | ||||||
|  |     if (userInput === "y") { | ||||||
|  |       // Create the app-example directory | ||||||
|  |       await fs.promises.mkdir(exampleDirPath, { recursive: true }); | ||||||
|  |       console.log(`📁 /${exampleDir} directory created.`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Move old directories to new app-example directory or delete them | ||||||
|  |     for (const dir of oldDirs) { | ||||||
|  |       const oldDirPath = path.join(root, dir); | ||||||
|  |       if (fs.existsSync(oldDirPath)) { | ||||||
|  |         if (userInput === "y") { | ||||||
|  |           const newDirPath = path.join(root, exampleDir, dir); | ||||||
|  |           await fs.promises.rename(oldDirPath, newDirPath); | ||||||
|  |           console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`); | ||||||
|  |         } else { | ||||||
|  |           await fs.promises.rm(oldDirPath, { recursive: true, force: true }); | ||||||
|  |           console.log(`❌ /${dir} deleted.`); | ||||||
|  |         } | ||||||
|  |       } 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.${ | ||||||
|  |         userInput === "y" | ||||||
|  |           ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` | ||||||
|  |           : "" | ||||||
|  |       }` | ||||||
|  |     ); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error(`❌ Error during script execution: ${error.message}`); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | rl.question( | ||||||
|  |   "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ", | ||||||
|  |   (answer) => { | ||||||
|  |     const userInput = answer.trim().toLowerCase() || "y"; | ||||||
|  |     if (userInput === "y" || userInput === "n") { | ||||||
|  |       moveDirectories(userInput).finally(() => rl.close()); | ||||||
|  |     } else { | ||||||
|  |       console.log("❌ Invalid input. Please enter 'Y' or 'N'."); | ||||||
|  |       rl.close(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | ); | ||||||
							
								
								
									
										17
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | |||||||
|  | { | ||||||
|  |   "extends": "expo/tsconfig.base", | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "strict": true, | ||||||
|  |     "paths": { | ||||||
|  |       "@/*": [ | ||||||
|  |         "./*" | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "include": [ | ||||||
|  |     "**/*.ts", | ||||||
|  |     "**/*.tsx", | ||||||
|  |     ".expo/types/**/*.ts", | ||||||
|  |     "expo-env.d.ts" | ||||||
|  |   ] | ||||||
|  | } | ||||||