We are on Build a screen
6
.gitignore
vendored
@ -12,3 +12,9 @@ web-build/
|
|||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
|
||||||
|
# The following patterns were generated by expo-cli
|
||||||
|
|
||||||
|
expo-env.d.ts
|
||||||
|
# @end expo-cli
|
@ -1,21 +1,28 @@
|
|||||||
import { Tabs } from 'expo-router';
|
import { Tabs } from "expo-router";
|
||||||
import React from 'react';
|
import { TabBarIcon } from '@/components/default/navigation/TabBarIcon';
|
||||||
|
|
||||||
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
|
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
|
||||||
export default function TabLayout() {
|
const TabLayout = () => {
|
||||||
const colorScheme = useColorScheme();
|
const scheme = useColorScheme() ?? 'light';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
|
tabBarActiveTintColor: Colors[scheme].tint,
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}>
|
headerStyle: {
|
||||||
|
backgroundColor: Colors[scheme].background,
|
||||||
|
},
|
||||||
|
headerShadowVisible: false,
|
||||||
|
headerTintColor: Colors[scheme].tint,
|
||||||
|
tabBarStyle: {
|
||||||
|
backgroundColor: Colors[scheme].background,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name='index'
|
||||||
options={{
|
options={{
|
||||||
title: 'Home',
|
title: 'Home',
|
||||||
tabBarIcon: ({ color, focused }) => (
|
tabBarIcon: ({ color, focused }) => (
|
||||||
@ -24,14 +31,15 @@ export default function TabLayout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="explore"
|
name='settings'
|
||||||
options={{
|
options={{
|
||||||
title: 'Explore',
|
title: 'Settings',
|
||||||
tabBarIcon: ({ color, focused }) => (
|
tabBarIcon: ({ color, focused }) => (
|
||||||
<TabBarIcon name={focused ? 'code-slash' : 'code-slash-outline'} color={color} />
|
<TabBarIcon name={focused ? 'settings' : 'settings-outline'} color={color} />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
export default TabLayout;
|
||||||
|
@ -1,70 +1,34 @@
|
|||||||
import { Image, StyleSheet, Platform } from 'react-native';
|
import { StyleSheet } from "react-native";
|
||||||
|
import { ThemedText } from "@/components/ThemedText";
|
||||||
|
import { ThemedView } from "@/components/ThemedView";
|
||||||
|
|
||||||
import { HelloWave } from '@/components/HelloWave';
|
import Button from "@/components/buttons/Button";
|
||||||
import ParallaxScrollView from '@/components/ParallaxScrollView';
|
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
|
||||||
import { ThemedView } from '@/components/ThemedView';
|
|
||||||
|
|
||||||
export default function HomeScreen() {
|
const Index = () => {
|
||||||
return (
|
return (
|
||||||
<ParallaxScrollView
|
<ThemedView style={styles.container}>
|
||||||
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
|
<ThemedText style={styles.text}>
|
||||||
headerImage={
|
Home Screen
|
||||||
<Image
|
</ThemedText>
|
||||||
source={require('@/assets/images/partial-react-logo.png')}
|
<ThemedView style={styles.footerContainer}>
|
||||||
style={styles.reactLogo}
|
<Button label="Choose a photo" />
|
||||||
/>
|
|
||||||
}>
|
|
||||||
<ThemedView style={styles.titleContainer}>
|
|
||||||
<ThemedText type="title">Welcome!</ThemedText>
|
|
||||||
<HelloWave />
|
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
<ThemedView style={styles.stepContainer}>
|
</ThemedView>
|
||||||
<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' })}
|
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export default Index;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
titleContainer: {
|
container: {
|
||||||
flexDirection: 'row',
|
flex: 1,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 8,
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
stepContainer: {
|
text: {
|
||||||
gap: 8,
|
fontSize: 24,
|
||||||
marginBottom: 8,
|
|
||||||
},
|
},
|
||||||
reactLogo: {
|
footerContainer: {
|
||||||
height: 178,
|
flex: 1 / 3,
|
||||||
width: 290,
|
alignItems: 'center',
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
position: 'absolute',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
25
app/(tabs)/settings.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
import { ThemedText } from "@/components/ThemedText";
|
||||||
|
import { ThemedView } from "@/components/ThemedView";
|
||||||
|
|
||||||
|
const Settings = () => {
|
||||||
|
return (
|
||||||
|
<ThemedView style={styles.container}>
|
||||||
|
<ThemedText style={styles.text}>
|
||||||
|
Settings
|
||||||
|
</ThemedText>
|
||||||
|
</ThemedView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Settings;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 24,
|
||||||
|
},
|
||||||
|
});
|
@ -1,32 +1,32 @@
|
|||||||
import { Link, Stack } from 'expo-router';
|
import { StyleSheet } from "react-native";
|
||||||
import { StyleSheet } from 'react-native';
|
import { ThemedText } from "@/components/ThemedText";
|
||||||
|
import { ThemedView } from "@/components/ThemedView";
|
||||||
|
import { Link, Stack } from "expo-router";
|
||||||
|
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
const NotFoundScreen = () => {
|
||||||
import { ThemedView } from '@/components/ThemedView';
|
|
||||||
|
|
||||||
export default function NotFoundScreen() {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
<Stack.Screen options={{ title: 'Page not found.' }} />
|
||||||
<ThemedView style={styles.container}>
|
<ThemedView style={styles.container}>
|
||||||
<ThemedText type="title">This screen doesn't exist.</ThemedText>
|
<Link href='/'>
|
||||||
<Link href="/" style={styles.link}>
|
<ThemedText style={styles.button}>
|
||||||
<ThemedText type="link">Go to home screen!</ThemedText>
|
Go back home.
|
||||||
|
</ThemedText>
|
||||||
</Link>
|
</Link>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
export default NotFoundScreen;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: 20,
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
link: {
|
button: {
|
||||||
marginTop: 15,
|
fontSize: 24,
|
||||||
paddingVertical: 15,
|
textDecorationLine: 'underline',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,37 +1,10 @@
|
|||||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
import { Stack } from "expo-router";
|
||||||
import { useFonts } from 'expo-font';
|
|
||||||
import { Stack } from 'expo-router';
|
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import 'react-native-reanimated';
|
|
||||||
|
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
||||||
|
|
||||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
|
||||||
SplashScreen.preventAutoHideAsync();
|
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
const [loaded] = useFonts({
|
|
||||||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (loaded) {
|
|
||||||
SplashScreen.hideAsync();
|
|
||||||
}
|
|
||||||
}, [loaded]);
|
|
||||||
|
|
||||||
if (!loaded) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
<Stack>
|
||||||
<Stack>
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
<Stack.Screen name="+not-found" />
|
||||||
<Stack.Screen name="+not-found" />
|
</Stack>
|
||||||
</Stack>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.0 MiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 4.0 MiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 2.6 MiB |
@ -1,37 +0,0 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
|
||||||
import Animated, {
|
|
||||||
useSharedValue,
|
|
||||||
useAnimatedStyle,
|
|
||||||
withTiming,
|
|
||||||
withRepeat,
|
|
||||||
withSequence,
|
|
||||||
} from 'react-native-reanimated';
|
|
||||||
|
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
|
||||||
|
|
||||||
export function HelloWave() {
|
|
||||||
const rotationAnimation = useSharedValue(0);
|
|
||||||
|
|
||||||
rotationAnimation.value = withRepeat(
|
|
||||||
withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
|
|
||||||
4 // Run the animation 4 times
|
|
||||||
);
|
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => ({
|
|
||||||
transform: [{ rotate: `${rotationAnimation.value}deg` }],
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View style={animatedStyle}>
|
|
||||||
<ThemedText style={styles.text}>👋</ThemedText>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
text: {
|
|
||||||
fontSize: 28,
|
|
||||||
lineHeight: 32,
|
|
||||||
marginTop: -6,
|
|
||||||
},
|
|
||||||
});
|
|
85
components/buttons/Button.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { StyleSheet, Pressable } from "react-native";
|
||||||
|
import { ThemedText } from "@/components/ThemedText";
|
||||||
|
import { ThemedView } from "@/components/ThemedView";
|
||||||
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string;
|
||||||
|
theme?: 'primary';
|
||||||
|
onPress?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Button = ({ label, theme, onPress }: Props) => {
|
||||||
|
const scheme = useColorScheme() ?? 'light';
|
||||||
|
if (theme === 'primary') {
|
||||||
|
return (
|
||||||
|
<ThemedView style={styles.buttonContainer}>
|
||||||
|
<Pressable
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
{backgroundColor: Colors[scheme].text}
|
||||||
|
]}
|
||||||
|
onPress={() => alert('You pressed a button.')}
|
||||||
|
>
|
||||||
|
<ThemedText
|
||||||
|
style={[
|
||||||
|
styles.buttonLabel,
|
||||||
|
{color: Colors[scheme].background}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
</ThemedView>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<ThemedView style={styles.buttonContainer}>
|
||||||
|
<Pressable
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
{backgroundColor: Colors[scheme].text}
|
||||||
|
]}
|
||||||
|
onPress={() => alert('You pressed a button.')}
|
||||||
|
>
|
||||||
|
<ThemedText
|
||||||
|
style={[
|
||||||
|
styles.buttonLabel,
|
||||||
|
{color: Colors[scheme].background}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
</ThemedView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default Button;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
buttonContainer: {
|
||||||
|
width: 320,
|
||||||
|
height: 68,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 3,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
borderRadius: 10,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
buttonIcon: {
|
||||||
|
paddingRight: 8,
|
||||||
|
},
|
||||||
|
buttonLabel: {
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
});
|
@ -17,7 +17,7 @@ export const Colors = {
|
|||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
text: '#ECEDEE',
|
text: '#ECEDEE',
|
||||||
background: '#151718',
|
background: '#2e2f3d',
|
||||||
tint: tintColorDark,
|
tint: tintColorDark,
|
||||||
icon: '#9BA1A6',
|
icon: '#9BA1A6',
|
||||||
tabIconDefault: '#9BA1A6',
|
tabIconDefault: '#9BA1A6',
|
||||||
|
37
example/app/(tabs)/_layout.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Tabs } from 'expo-router';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
|
||||||
|
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,
|
||||||
|
}}>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="index"
|
||||||
|
options={{
|
||||||
|
title: 'Home',
|
||||||
|
tabBarIcon: ({ color, focused }) => (
|
||||||
|
<TabBarIcon name={focused ? 'home' : 'home-outline'} color={color} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="explore"
|
||||||
|
options={{
|
||||||
|
title: 'Explore',
|
||||||
|
tabBarIcon: ({ color, focused }) => (
|
||||||
|
<TabBarIcon name={focused ? 'code-slash' : 'code-slash-outline'} color={color} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
70
example/app/(tabs)/index.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { Image, StyleSheet, Platform } from 'react-native';
|
||||||
|
|
||||||
|
import { HelloWave } from '@/components/HelloWave';
|
||||||
|
import ParallaxScrollView from '@/components/ParallaxScrollView';
|
||||||
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
|
import { ThemedView } from '@/components/ThemedView';
|
||||||
|
|
||||||
|
export default function HomeScreen() {
|
||||||
|
return (
|
||||||
|
<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' })}
|
||||||
|
</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
example/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 doesn't 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,
|
||||||
|
},
|
||||||
|
});
|
37
example/app/_layout.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||||
|
import { useFonts } from 'expo-font';
|
||||||
|
import { Stack } from 'expo-router';
|
||||||
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import 'react-native-reanimated';
|
||||||
|
|
||||||
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
|
||||||
|
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||||
|
SplashScreen.preventAutoHideAsync();
|
||||||
|
|
||||||
|
export default function RootLayout() {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const [loaded] = useFonts({
|
||||||
|
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loaded) {
|
||||||
|
SplashScreen.hideAsync();
|
||||||
|
}
|
||||||
|
}, [loaded]);
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||||
|
<Stack>
|
||||||
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="+not-found" />
|
||||||
|
</Stack>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
BIN
example/assets/fonts/SpaceMono-Regular.ttf
Executable file
BIN
example/assets/images/adaptive-icon.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
example/assets/images/favicon.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
example/assets/images/icon.png
Normal file
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
BIN
example/assets/images/splash.png
Normal file
After Width: | Height: | Size: 46 KiB |
41
example/components/Collapsible.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||||
|
import { PropsWithChildren, useState } from 'react';
|
||||||
|
import { StyleSheet, TouchableOpacity, useColorScheme } from 'react-native';
|
||||||
|
|
||||||
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
|
import { ThemedView } from '@/components/ThemedView';
|
||||||
|
import { Colors } from '@/constants/Colors';
|
||||||
|
|
||||||
|
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}>
|
||||||
|
<Ionicons
|
||||||
|
name={isOpen ? 'chevron-down' : 'chevron-forward-outline'}
|
||||||
|
size={18}
|
||||||
|
color={theme === 'light' ? Colors.light.icon : Colors.dark.icon}
|
||||||
|
/>
|
||||||
|
<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
example/components/ExternalLink.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Link } from 'expo-router';
|
||||||
|
import { openBrowserAsync } from 'expo-web-browser';
|
||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
|
type Props = Omit<ComponentProps<typeof Link>, '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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
76
example/components/ParallaxScrollView.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import type { PropsWithChildren, ReactElement } from 'react';
|
||||||
|
import { StyleSheet, useColorScheme } from 'react-native';
|
||||||
|
import Animated, {
|
||||||
|
interpolate,
|
||||||
|
useAnimatedRef,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useScrollViewOffset,
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
|
||||||
|
import { ThemedView } from '@/components/ThemedView';
|
||||||
|
|
||||||
|
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 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}>
|
||||||
|
<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: 250,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 32,
|
||||||
|
gap: 16,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
});
|
60
example/components/ThemedText.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Text, type TextProps, StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||||
|
|
||||||
|
export type ThemedTextProps = TextProps & {
|
||||||
|
lightColor?: string;
|
||||||
|
darkColor?: string;
|
||||||
|
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ThemedText({
|
||||||
|
style,
|
||||||
|
lightColor,
|
||||||
|
darkColor,
|
||||||
|
type = 'default',
|
||||||
|
...rest
|
||||||
|
}: ThemedTextProps) {
|
||||||
|
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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
example/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} />;
|
||||||
|
}
|
9
example/components/navigation/TabBarIcon.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
||||||
|
|
||||||
|
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||||
|
import { type IconProps } from '@expo/vector-icons/build/createIconSet';
|
||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export function TabBarIcon({ style, ...rest }: IconProps<ComponentProps<typeof Ionicons>['name']>) {
|
||||||
|
return <Ionicons size={28} style={[{ marginBottom: -3 }, style]} {...rest} />;
|
||||||
|
}
|
26
example/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,
|
||||||
|
},
|
||||||
|
};
|
1
example/hooks/useColorScheme.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { useColorScheme } from 'react-native';
|
8
example/hooks/useColorScheme.web.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// NOTE: The default React Native styling doesn't support server rendering.
|
||||||
|
// Server rendered styles should not change between the first render of the HTML
|
||||||
|
// and the first render on the client. Typically, web developers will use CSS media queries
|
||||||
|
// to render different styles on the client and server, these aren't directly supported in React Native
|
||||||
|
// but can be achieved using a styling library like Nativewind.
|
||||||
|
export function useColorScheme() {
|
||||||
|
return 'light';
|
||||||
|
}
|
22
example/hooks/useThemeColor.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Learn more about light and dark modes:
|
||||||
|
* https://docs.expo.dev/guides/color-schemes/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useColorScheme } from 'react-native';
|
||||||
|
|
||||||
|
import { Colors } from '@/constants/Colors';
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
10
package-lock.json
generated
@ -13,6 +13,7 @@
|
|||||||
"expo": "~51.0.28",
|
"expo": "~51.0.28",
|
||||||
"expo-constants": "~16.0.2",
|
"expo-constants": "~16.0.2",
|
||||||
"expo-font": "~12.0.9",
|
"expo-font": "~12.0.9",
|
||||||
|
"expo-image": "~1.13.0",
|
||||||
"expo-linking": "~6.3.1",
|
"expo-linking": "~6.3.1",
|
||||||
"expo-router": "~3.5.23",
|
"expo-router": "~3.5.23",
|
||||||
"expo-splash-screen": "~0.27.5",
|
"expo-splash-screen": "~0.27.5",
|
||||||
@ -10102,6 +10103,15 @@
|
|||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-image": {
|
||||||
|
"version": "1.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-image/-/expo-image-1.13.0.tgz",
|
||||||
|
"integrity": "sha512-0NLDcFmEn4Nh1sXeRvNzDHT+Fl6FXtTol6ki6kYYH0/iDeSFWyIy/Fek6kzDDYAmhipSMR7buPf7VVoHseTbAA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-keep-awake": {
|
"node_modules/expo-keep-awake": {
|
||||||
"version": "13.0.2",
|
"version": "13.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-13.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-13.0.2.tgz",
|
||||||
|
@ -33,7 +33,8 @@
|
|||||||
"react-native-reanimated": "~3.10.1",
|
"react-native-reanimated": "~3.10.1",
|
||||||
"react-native-safe-area-context": "4.10.5",
|
"react-native-safe-area-context": "4.10.5",
|
||||||
"react-native-screens": "3.31.1",
|
"react-native-screens": "3.31.1",
|
||||||
"react-native-web": "~0.19.10"
|
"react-native-web": "~0.19.10",
|
||||||
|
"expo-image": "~1.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
|