@ -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 |
||||
} |
||||