@ -0,0 +1,43 @@ |
|||||||
|
# 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 |
||||||
|
|
||||||
|
# generated native folders |
||||||
|
/ios |
||||||
|
/android |
||||||
@ -0,0 +1 @@ |
|||||||
|
{ "recommendations": ["expo.vscode-expo-tools"] } |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
{ |
||||||
|
"editor.codeActionsOnSave": { |
||||||
|
"source.fixAll": "explicit", |
||||||
|
"source.organizeImports": "explicit", |
||||||
|
"source.sortMembers": "explicit" |
||||||
|
} |
||||||
|
} |
||||||
@ -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. |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
{ |
||||||
|
"expo": { |
||||||
|
"name": "my-app", |
||||||
|
"slug": "my-app", |
||||||
|
"version": "1.0.0", |
||||||
|
"orientation": "portrait", |
||||||
|
"icon": "./assets/images/icon.png", |
||||||
|
"scheme": "myapp", |
||||||
|
"userInterfaceStyle": "automatic", |
||||||
|
"newArchEnabled": true, |
||||||
|
"ios": { |
||||||
|
"supportsTablet": true |
||||||
|
}, |
||||||
|
"android": { |
||||||
|
"adaptiveIcon": { |
||||||
|
"backgroundColor": "#E6F4FE", |
||||||
|
"foregroundImage": "./assets/images/android-icon-foreground.png", |
||||||
|
"backgroundImage": "./assets/images/android-icon-background.png", |
||||||
|
"monochromeImage": "./assets/images/android-icon-monochrome.png" |
||||||
|
}, |
||||||
|
"edgeToEdgeEnabled": true, |
||||||
|
"predictiveBackGestureEnabled": false |
||||||
|
}, |
||||||
|
"web": { |
||||||
|
"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", |
||||||
|
"dark": { |
||||||
|
"backgroundColor": "#000000" |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
], |
||||||
|
"experiments": { |
||||||
|
"typedRoutes": true, |
||||||
|
"reactCompiler": true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
import { Tabs } from 'expo-router'; |
||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
import { HapticTab } from '@/components/haptic-tab'; |
||||||
|
import { IconSymbol } from '@/components/ui/icon-symbol'; |
||||||
|
import { Colors } from '@/constants/theme'; |
||||||
|
import { useColorScheme } from '@/hooks/use-color-scheme'; |
||||||
|
|
||||||
|
export default function TabLayout() { |
||||||
|
const colorScheme = useColorScheme(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Tabs |
||||||
|
screenOptions={{ |
||||||
|
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, |
||||||
|
headerShown: false, |
||||||
|
tabBarButton: HapticTab, |
||||||
|
}}> |
||||||
|
<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> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,112 @@ |
|||||||
|
import { Image } from 'expo-image'; |
||||||
|
import { Platform, StyleSheet } from 'react-native'; |
||||||
|
|
||||||
|
import { Collapsible } from '@/components/ui/collapsible'; |
||||||
|
import { ExternalLink } from '@/components/external-link'; |
||||||
|
import ParallaxScrollView from '@/components/parallax-scroll-view'; |
||||||
|
import { ThemedText } from '@/components/themed-text'; |
||||||
|
import { ThemedView } from '@/components/themed-view'; |
||||||
|
import { IconSymbol } from '@/components/ui/icon-symbol'; |
||||||
|
import { Fonts } from '@/constants/theme'; |
||||||
|
|
||||||
|
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" |
||||||
|
style={{ |
||||||
|
fontFamily: Fonts.rounded, |
||||||
|
}}> |
||||||
|
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={{ width: 100, height: 100, alignSelf: 'center' }} |
||||||
|
/> |
||||||
|
<ExternalLink href="https://reactnative.dev/docs/images"> |
||||||
|
<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" style={{ fontFamily: Fonts.mono }}> |
||||||
|
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, |
||||||
|
}, |
||||||
|
}); |
||||||
@ -0,0 +1,98 @@ |
|||||||
|
import { Image } from 'expo-image'; |
||||||
|
import { Platform, StyleSheet } from 'react-native'; |
||||||
|
|
||||||
|
import { HelloWave } from '@/components/hello-wave'; |
||||||
|
import ParallaxScrollView from '@/components/parallax-scroll-view'; |
||||||
|
import { ThemedText } from '@/components/themed-text'; |
||||||
|
import { ThemedView } from '@/components/themed-view'; |
||||||
|
import { Link } from 'expo-router'; |
||||||
|
|
||||||
|
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}> |
||||||
|
<Link href="/modal"> |
||||||
|
<Link.Trigger> |
||||||
|
<ThemedText type="subtitle">Step 2: Explore</ThemedText> |
||||||
|
</Link.Trigger> |
||||||
|
<Link.Preview /> |
||||||
|
<Link.Menu> |
||||||
|
<Link.MenuAction title="Action" icon="cube" onPress={() => alert('Action pressed')} /> |
||||||
|
<Link.MenuAction |
||||||
|
title="Share" |
||||||
|
icon="square.and.arrow.up" |
||||||
|
onPress={() => alert('Share pressed')} |
||||||
|
/> |
||||||
|
<Link.Menu title="More" icon="ellipsis"> |
||||||
|
<Link.MenuAction |
||||||
|
title="Delete" |
||||||
|
icon="trash" |
||||||
|
destructive |
||||||
|
onPress={() => alert('Delete pressed')} |
||||||
|
/> |
||||||
|
</Link.Menu> |
||||||
|
</Link.Menu> |
||||||
|
</Link> |
||||||
|
|
||||||
|
<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', |
||||||
|
}, |
||||||
|
}); |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; |
||||||
|
import { Stack } from 'expo-router'; |
||||||
|
import { StatusBar } from 'expo-status-bar'; |
||||||
|
import 'react-native-reanimated'; |
||||||
|
|
||||||
|
import { useColorScheme } from '@/hooks/use-color-scheme'; |
||||||
|
|
||||||
|
export const unstable_settings = { |
||||||
|
anchor: '(tabs)', |
||||||
|
}; |
||||||
|
|
||||||
|
export default function RootLayout() { |
||||||
|
const colorScheme = useColorScheme(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> |
||||||
|
<Stack> |
||||||
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> |
||||||
|
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} /> |
||||||
|
</Stack> |
||||||
|
<StatusBar style="auto" /> |
||||||
|
</ThemeProvider> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,29 @@ |
|||||||
|
import { Link } from 'expo-router'; |
||||||
|
import { StyleSheet } from 'react-native'; |
||||||
|
|
||||||
|
import { ThemedText } from '@/components/themed-text'; |
||||||
|
import { ThemedView } from '@/components/themed-view'; |
||||||
|
|
||||||
|
export default function ModalScreen() { |
||||||
|
return ( |
||||||
|
<ThemedView style={styles.container}> |
||||||
|
<ThemedText type="title">This is a modal</ThemedText> |
||||||
|
<Link href="/" dismissTo 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, |
||||||
|
}, |
||||||
|
}); |
||||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 384 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 17 KiB |
@ -0,0 +1,25 @@ |
|||||||
|
import { Href, Link } from 'expo-router'; |
||||||
|
import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser'; |
||||||
|
import { type ComponentProps } from 'react'; |
||||||
|
|
||||||
|
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 (process.env.EXPO_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, { |
||||||
|
presentationStyle: WebBrowserPresentationStyle.AUTOMATIC, |
||||||
|
}); |
||||||
|
} |
||||||
|
}} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
@ -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); |
||||||
|
}} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
import Animated from 'react-native-reanimated'; |
||||||
|
|
||||||
|
export function HelloWave() { |
||||||
|
return ( |
||||||
|
<Animated.Text |
||||||
|
style={{ |
||||||
|
fontSize: 28, |
||||||
|
lineHeight: 32, |
||||||
|
marginTop: -6, |
||||||
|
animationName: { |
||||||
|
'50%': { transform: [{ rotate: '25deg' }] }, |
||||||
|
}, |
||||||
|
animationIterationCount: 4, |
||||||
|
animationDuration: '300ms', |
||||||
|
}}> |
||||||
|
👋 |
||||||
|
</Animated.Text> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,79 @@ |
|||||||
|
import type { PropsWithChildren, ReactElement } from 'react'; |
||||||
|
import { StyleSheet } from 'react-native'; |
||||||
|
import Animated, { |
||||||
|
interpolate, |
||||||
|
useAnimatedRef, |
||||||
|
useAnimatedStyle, |
||||||
|
useScrollOffset, |
||||||
|
} from 'react-native-reanimated'; |
||||||
|
|
||||||
|
import { ThemedView } from '@/components/themed-view'; |
||||||
|
import { useColorScheme } from '@/hooks/use-color-scheme'; |
||||||
|
import { useThemeColor } from '@/hooks/use-theme-color'; |
||||||
|
|
||||||
|
const HEADER_HEIGHT = 250; |
||||||
|
|
||||||
|
type Props = PropsWithChildren<{ |
||||||
|
headerImage: ReactElement; |
||||||
|
headerBackgroundColor: { dark: string; light: string }; |
||||||
|
}>; |
||||||
|
|
||||||
|
export default function ParallaxScrollView({ |
||||||
|
children, |
||||||
|
headerImage, |
||||||
|
headerBackgroundColor, |
||||||
|
}: Props) { |
||||||
|
const backgroundColor = useThemeColor({}, 'background'); |
||||||
|
const colorScheme = useColorScheme() ?? 'light'; |
||||||
|
const scrollRef = useAnimatedRef<Animated.ScrollView>(); |
||||||
|
const scrollOffset = useScrollOffset(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 ( |
||||||
|
<Animated.ScrollView |
||||||
|
ref={scrollRef} |
||||||
|
style={{ backgroundColor, flex: 1 }} |
||||||
|
scrollEventThrottle={16}> |
||||||
|
<Animated.View |
||||||
|
style={[ |
||||||
|
styles.header, |
||||||
|
{ backgroundColor: headerBackgroundColor[colorScheme] }, |
||||||
|
headerAnimatedStyle, |
||||||
|
]}> |
||||||
|
{headerImage} |
||||||
|
</Animated.View> |
||||||
|
<ThemedView style={styles.content}>{children}</ThemedView> |
||||||
|
</Animated.ScrollView> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const styles = StyleSheet.create({ |
||||||
|
container: { |
||||||
|
flex: 1, |
||||||
|
}, |
||||||
|
header: { |
||||||
|
height: HEADER_HEIGHT, |
||||||
|
overflow: 'hidden', |
||||||
|
}, |
||||||
|
content: { |
||||||
|
flex: 1, |
||||||
|
padding: 32, |
||||||
|
gap: 16, |
||||||
|
overflow: 'hidden', |
||||||
|
}, |
||||||
|
}); |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
import { StyleSheet, Text, type TextProps } from 'react-native'; |
||||||
|
|
||||||
|
import { useThemeColor } from '@/hooks/use-theme-color'; |
||||||
|
|
||||||
|
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', |
||||||
|
}, |
||||||
|
}); |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
import { View, type ViewProps } from 'react-native'; |
||||||
|
|
||||||
|
import { useThemeColor } from '@/hooks/use-theme-color'; |
||||||
|
|
||||||
|
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} />; |
||||||
|
} |
||||||
@ -0,0 +1,45 @@ |
|||||||
|
import { PropsWithChildren, useState } from 'react'; |
||||||
|
import { StyleSheet, TouchableOpacity } from 'react-native'; |
||||||
|
|
||||||
|
import { ThemedText } from '@/components/themed-text'; |
||||||
|
import { ThemedView } from '@/components/themed-view'; |
||||||
|
import { IconSymbol } from '@/components/ui/icon-symbol'; |
||||||
|
import { Colors } from '@/constants/theme'; |
||||||
|
import { useColorScheme } from '@/hooks/use-color-scheme'; |
||||||
|
|
||||||
|
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, |
||||||
|
}, |
||||||
|
}); |
||||||
@ -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, |
||||||
|
]} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
@ -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} />; |
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
/** |
||||||
|
* 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.
|
||||||
|
*/ |
||||||
|
|
||||||
|
import { Platform } from 'react-native'; |
||||||
|
|
||||||
|
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, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
export const Fonts = Platform.select({ |
||||||
|
ios: { |
||||||
|
/** iOS `UIFontDescriptorSystemDesignDefault` */ |
||||||
|
sans: 'system-ui', |
||||||
|
/** iOS `UIFontDescriptorSystemDesignSerif` */ |
||||||
|
serif: 'ui-serif', |
||||||
|
/** iOS `UIFontDescriptorSystemDesignRounded` */ |
||||||
|
rounded: 'ui-rounded', |
||||||
|
/** iOS `UIFontDescriptorSystemDesignMonospaced` */ |
||||||
|
mono: 'ui-monospace', |
||||||
|
}, |
||||||
|
default: { |
||||||
|
sans: 'normal', |
||||||
|
serif: 'serif', |
||||||
|
rounded: 'normal', |
||||||
|
mono: 'monospace', |
||||||
|
}, |
||||||
|
web: { |
||||||
|
sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif", |
||||||
|
serif: "Georgia, 'Times New Roman', serif", |
||||||
|
rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif", |
||||||
|
mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", |
||||||
|
}, |
||||||
|
}); |
||||||
@ -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/*'], |
||||||
|
}, |
||||||
|
]); |
||||||
@ -0,0 +1 @@ |
|||||||
|
export { useColorScheme } from 'react-native'; |
||||||
@ -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'; |
||||||
|
} |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
/** |
||||||
|
* Learn more about light and dark modes: |
||||||
|
* https://docs.expo.dev/guides/color-schemes/
|
||||||
|
*/ |
||||||
|
|
||||||
|
import { Colors } from '@/constants/theme'; |
||||||
|
import { useColorScheme } from '@/hooks/use-color-scheme'; |
||||||
|
|
||||||
|
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]; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,47 @@ |
|||||||
|
{ |
||||||
|
"name": "my-app", |
||||||
|
"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": "^15.0.3", |
||||||
|
"@react-navigation/bottom-tabs": "^7.4.0", |
||||||
|
"@react-navigation/elements": "^2.6.3", |
||||||
|
"@react-navigation/native": "^7.1.8", |
||||||
|
"expo": "~54.0.22", |
||||||
|
"expo-constants": "~18.0.10", |
||||||
|
"expo-font": "~14.0.9", |
||||||
|
"expo-haptics": "~15.0.7", |
||||||
|
"expo-image": "~3.0.10", |
||||||
|
"expo-linking": "~8.0.8", |
||||||
|
"expo-router": "~6.0.14", |
||||||
|
"expo-splash-screen": "~31.0.10", |
||||||
|
"expo-status-bar": "~3.0.8", |
||||||
|
"expo-symbols": "~1.0.7", |
||||||
|
"expo-system-ui": "~6.0.8", |
||||||
|
"expo-web-browser": "~15.0.9", |
||||||
|
"react": "19.1.0", |
||||||
|
"react-dom": "19.1.0", |
||||||
|
"react-native": "0.81.5", |
||||||
|
"react-native-gesture-handler": "~2.28.0", |
||||||
|
"react-native-reanimated": "~4.1.1", |
||||||
|
"react-native-safe-area-context": "~5.6.0", |
||||||
|
"react-native-screens": "~4.16.0", |
||||||
|
"react-native-web": "~0.21.0", |
||||||
|
"react-native-worklets": "0.5.1" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@types/react": "~19.1.0", |
||||||
|
"eslint": "^9.25.0", |
||||||
|
"eslint-config-expo": "~10.0.0", |
||||||
|
"typescript": "~5.9.2" |
||||||
|
}, |
||||||
|
"private": true |
||||||
|
} |
||||||