parent
7d3704bb4d
commit
c71bd5462a
@ -0,0 +1,37 @@ |
||||
# 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 |
@ -0,0 +1,38 @@ |
||||
{ |
||||
"expo": { |
||||
"name": "my-expo-sample", |
||||
"slug": "my-expo-sample", |
||||
"version": "1.0.0", |
||||
"orientation": "portrait", |
||||
"icon": "./assets/images/icon.png", |
||||
"scheme": "myexposample", |
||||
"userInterfaceStyle": "automatic", |
||||
"newArchEnabled": true, |
||||
"splash": { |
||||
"image": "./assets/images/splash-icon.png", |
||||
"resizeMode": "contain", |
||||
"backgroundColor": "#ffffff" |
||||
}, |
||||
"ios": { |
||||
"supportsTablet": true |
||||
}, |
||||
"android": { |
||||
"adaptiveIcon": { |
||||
"foregroundImage": "./assets/images/adaptive-icon.png", |
||||
"backgroundColor": "#ffffff" |
||||
}, |
||||
"edgeToEdgeEnabled": true |
||||
}, |
||||
"web": { |
||||
"bundler": "metro", |
||||
"output": "static", |
||||
"favicon": "./assets/images/favicon.png" |
||||
}, |
||||
"plugins": [ |
||||
"expo-router" |
||||
], |
||||
"experiments": { |
||||
"typedRoutes": true |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,59 @@ |
||||
import React from 'react'; |
||||
import FontAwesome from '@expo/vector-icons/FontAwesome'; |
||||
import { Link, Tabs } from 'expo-router'; |
||||
import { Pressable } from 'react-native'; |
||||
|
||||
import Colors from '@/constants/Colors'; |
||||
import { useColorScheme } from '@/components/useColorScheme'; |
||||
import { useClientOnlyValue } from '@/components/useClientOnlyValue'; |
||||
|
||||
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
||||
function TabBarIcon(props: { |
||||
name: React.ComponentProps<typeof FontAwesome>['name']; |
||||
color: string; |
||||
}) { |
||||
return <FontAwesome size={28} style={{ marginBottom: -3 }} {...props} />; |
||||
} |
||||
|
||||
export default function TabLayout() { |
||||
const colorScheme = useColorScheme(); |
||||
|
||||
return ( |
||||
<Tabs |
||||
screenOptions={{ |
||||
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, |
||||
// Disable the static render of the header on web
|
||||
// to prevent a hydration error in React Navigation v6.
|
||||
headerShown: useClientOnlyValue(false, true), |
||||
}}> |
||||
<Tabs.Screen |
||||
name="index" |
||||
options={{ |
||||
title: 'Tab One', |
||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />, |
||||
headerRight: () => ( |
||||
<Link href="/modal" asChild> |
||||
<Pressable> |
||||
{({ pressed }) => ( |
||||
<FontAwesome |
||||
name="info-circle" |
||||
size={25} |
||||
color={Colors[colorScheme ?? 'light'].text} |
||||
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }} |
||||
/> |
||||
)} |
||||
</Pressable> |
||||
</Link> |
||||
), |
||||
}} |
||||
/> |
||||
<Tabs.Screen |
||||
name="two" |
||||
options={{ |
||||
title: 'Tab Two', |
||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />, |
||||
}} |
||||
/> |
||||
</Tabs> |
||||
); |
||||
} |
@ -0,0 +1,31 @@ |
||||
import { StyleSheet } from 'react-native'; |
||||
|
||||
import EditScreenInfo from '@/components/EditScreenInfo'; |
||||
import { Text, View } from '@/components/Themed'; |
||||
|
||||
export default function TabOneScreen() { |
||||
return ( |
||||
<View style={styles.container}> |
||||
<Text style={styles.title}>Tab One</Text> |
||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" /> |
||||
<EditScreenInfo path="app/(tabs)/index.tsx" /> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
flex: 1, |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
}, |
||||
title: { |
||||
fontSize: 20, |
||||
fontWeight: 'bold', |
||||
}, |
||||
separator: { |
||||
marginVertical: 30, |
||||
height: 1, |
||||
width: '80%', |
||||
}, |
||||
}); |
@ -0,0 +1,31 @@ |
||||
import { StyleSheet } from 'react-native'; |
||||
|
||||
import EditScreenInfo from '@/components/EditScreenInfo'; |
||||
import { Text, View } from '@/components/Themed'; |
||||
|
||||
export default function TabTwoScreen() { |
||||
return ( |
||||
<View style={styles.container}> |
||||
<Text style={styles.title}>Tab Two</Text> |
||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" /> |
||||
<EditScreenInfo path="app/(tabs)/two.tsx" /> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
flex: 1, |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
}, |
||||
title: { |
||||
fontSize: 20, |
||||
fontWeight: 'bold', |
||||
}, |
||||
separator: { |
||||
marginVertical: 30, |
||||
height: 1, |
||||
width: '80%', |
||||
}, |
||||
}); |
@ -0,0 +1,38 @@ |
||||
import { ScrollViewStyleReset } from 'expo-router/html'; |
||||
|
||||
// This file is web-only and used to configure the root HTML for every
|
||||
// web page during static rendering.
|
||||
// The contents of this function only run in Node.js environments and
|
||||
// do not have access to the DOM or browser APIs.
|
||||
export default function Root({ children }: { children: React.ReactNode }) { |
||||
return ( |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charSet="utf-8" /> |
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" /> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> |
||||
|
||||
{/* |
||||
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
|
||||
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. |
||||
*/} |
||||
<ScrollViewStyleReset /> |
||||
|
||||
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} |
||||
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} /> |
||||
{/* Add any additional <head> elements that you want globally available on web... */} |
||||
</head> |
||||
<body>{children}</body> |
||||
</html> |
||||
); |
||||
} |
||||
|
||||
const responsiveBackground = ` |
||||
body { |
||||
background-color: #fff; |
||||
} |
||||
@media (prefers-color-scheme: dark) { |
||||
body { |
||||
background-color: #000; |
||||
} |
||||
}`;
|
@ -0,0 +1,40 @@ |
||||
import { Link, Stack } from 'expo-router'; |
||||
import { StyleSheet } from 'react-native'; |
||||
|
||||
import { Text, View } from '@/components/Themed'; |
||||
|
||||
export default function NotFoundScreen() { |
||||
return ( |
||||
<> |
||||
<Stack.Screen options={{ title: 'Oops!' }} /> |
||||
<View style={styles.container}> |
||||
<Text style={styles.title}>This screen doesn't exist.</Text> |
||||
|
||||
<Link href="/" style={styles.link}> |
||||
<Text style={styles.linkText}>Go to home screen!</Text> |
||||
</Link> |
||||
</View> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
flex: 1, |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
padding: 20, |
||||
}, |
||||
title: { |
||||
fontSize: 20, |
||||
fontWeight: 'bold', |
||||
}, |
||||
link: { |
||||
marginTop: 15, |
||||
paddingVertical: 15, |
||||
}, |
||||
linkText: { |
||||
fontSize: 14, |
||||
color: '#2e78b7', |
||||
}, |
||||
}); |
@ -0,0 +1,59 @@ |
||||
import FontAwesome from '@expo/vector-icons/FontAwesome'; |
||||
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 '@/components/useColorScheme'; |
||||
|
||||
export { |
||||
// Catch any errors thrown by the Layout component.
|
||||
ErrorBoundary, |
||||
} from 'expo-router'; |
||||
|
||||
export const unstable_settings = { |
||||
// Ensure that reloading on `/modal` keeps a back button present.
|
||||
initialRouteName: '(tabs)', |
||||
}; |
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync(); |
||||
|
||||
export default function RootLayout() { |
||||
const [loaded, error] = useFonts({ |
||||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), |
||||
...FontAwesome.font, |
||||
}); |
||||
|
||||
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
|
||||
useEffect(() => { |
||||
if (error) throw error; |
||||
}, [error]); |
||||
|
||||
useEffect(() => { |
||||
if (loaded) { |
||||
SplashScreen.hideAsync(); |
||||
} |
||||
}, [loaded]); |
||||
|
||||
if (!loaded) { |
||||
return null; |
||||
} |
||||
|
||||
return <RootLayoutNav />; |
||||
} |
||||
|
||||
function RootLayoutNav() { |
||||
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' }} /> |
||||
</Stack> |
||||
</ThemeProvider> |
||||
); |
||||
} |
@ -0,0 +1,35 @@ |
||||
import { StatusBar } from 'expo-status-bar'; |
||||
import { Platform, StyleSheet } from 'react-native'; |
||||
|
||||
import EditScreenInfo from '@/components/EditScreenInfo'; |
||||
import { Text, View } from '@/components/Themed'; |
||||
|
||||
export default function ModalScreen() { |
||||
return ( |
||||
<View style={styles.container}> |
||||
<Text style={styles.title}>Modal</Text> |
||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" /> |
||||
<EditScreenInfo path="app/modal.tsx" /> |
||||
|
||||
{/* Use a light status bar on iOS to account for the black space above the modal */} |
||||
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} /> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
flex: 1, |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
}, |
||||
title: { |
||||
fontSize: 20, |
||||
fontWeight: 'bold', |
||||
}, |
||||
separator: { |
||||
marginVertical: 30, |
||||
height: 1, |
||||
width: '80%', |
||||
}, |
||||
}); |
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 17 KiB |
@ -0,0 +1,77 @@ |
||||
import React from 'react'; |
||||
import { StyleSheet } from 'react-native'; |
||||
|
||||
import { ExternalLink } from './ExternalLink'; |
||||
import { MonoText } from './StyledText'; |
||||
import { Text, View } from './Themed'; |
||||
|
||||
import Colors from '@/constants/Colors'; |
||||
|
||||
export default function EditScreenInfo({ path }: { path: string }) { |
||||
return ( |
||||
<View> |
||||
<View style={styles.getStartedContainer}> |
||||
<Text |
||||
style={styles.getStartedText} |
||||
lightColor="rgba(0,0,0,0.8)" |
||||
darkColor="rgba(255,255,255,0.8)"> |
||||
Open up the code for this screen: |
||||
</Text> |
||||
|
||||
<View |
||||
style={[styles.codeHighlightContainer, styles.homeScreenFilename]} |
||||
darkColor="rgba(255,255,255,0.05)" |
||||
lightColor="rgba(0,0,0,0.05)"> |
||||
<MonoText>{path}</MonoText> |
||||
</View> |
||||
|
||||
<Text |
||||
style={styles.getStartedText} |
||||
lightColor="rgba(0,0,0,0.8)" |
||||
darkColor="rgba(255,255,255,0.8)"> |
||||
Change any of the text, save the file, and your app will automatically update. |
||||
</Text> |
||||
</View> |
||||
|
||||
<View style={styles.helpContainer}> |
||||
<ExternalLink |
||||
style={styles.helpLink} |
||||
href="https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet"> |
||||
<Text style={styles.helpLinkText} lightColor={Colors.light.tint}> |
||||
Tap here if your app doesn't automatically update after making changes |
||||
</Text> |
||||
</ExternalLink> |
||||
</View> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
getStartedContainer: { |
||||
alignItems: 'center', |
||||
marginHorizontal: 50, |
||||
}, |
||||
homeScreenFilename: { |
||||
marginVertical: 7, |
||||
}, |
||||
codeHighlightContainer: { |
||||
borderRadius: 3, |
||||
paddingHorizontal: 4, |
||||
}, |
||||
getStartedText: { |
||||
fontSize: 17, |
||||
lineHeight: 24, |
||||
textAlign: 'center', |
||||
}, |
||||
helpContainer: { |
||||
marginTop: 15, |
||||
marginHorizontal: 20, |
||||
alignItems: 'center', |
||||
}, |
||||
helpLink: { |
||||
paddingVertical: 15, |
||||
}, |
||||
helpLinkText: { |
||||
textAlign: 'center', |
||||
}, |
||||
}); |
@ -0,0 +1,25 @@ |
||||
import { Link } from 'expo-router'; |
||||
import * as WebBrowser from 'expo-web-browser'; |
||||
import React from 'react'; |
||||
import { Platform } from 'react-native'; |
||||
|
||||
export function ExternalLink( |
||||
props: Omit<React.ComponentProps<typeof Link>, 'href'> & { href: string } |
||||
) { |
||||
return ( |
||||
<Link |
||||
target="_blank" |
||||
{...props} |
||||
// @ts-expect-error: External URLs are not typed.
|
||||
href={props.href} |
||||
onPress={(e) => { |
||||
if (Platform.OS !== 'web') { |
||||
// Prevent the default behavior of linking to the default browser on native.
|
||||
e.preventDefault(); |
||||
// Open the link in an in-app browser.
|
||||
WebBrowser.openBrowserAsync(props.href as string); |
||||
} |
||||
}} |
||||
/> |
||||
); |
||||
} |
@ -0,0 +1,5 @@ |
||||
import { Text, TextProps } from './Themed'; |
||||
|
||||
export function MonoText(props: TextProps) { |
||||
return <Text {...props} style={[props.style, { fontFamily: 'SpaceMono' }]} />; |
||||
} |
@ -0,0 +1,45 @@ |
||||
/** |
||||
* Learn more about Light and Dark modes: |
||||
* https://docs.expo.io/guides/color-schemes/
|
||||
*/ |
||||
|
||||
import { Text as DefaultText, View as DefaultView } from 'react-native'; |
||||
|
||||
import Colors from '@/constants/Colors'; |
||||
import { useColorScheme } from './useColorScheme'; |
||||
|
||||
type ThemeProps = { |
||||
lightColor?: string; |
||||
darkColor?: string; |
||||
}; |
||||
|
||||
export type TextProps = ThemeProps & DefaultText['props']; |
||||
export type ViewProps = ThemeProps & DefaultView['props']; |
||||
|
||||
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]; |
||||
} |
||||
} |
||||
|
||||
export function Text(props: TextProps) { |
||||
const { style, lightColor, darkColor, ...otherProps } = props; |
||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); |
||||
|
||||
return <DefaultText style={[{ color }, style]} {...otherProps} />; |
||||
} |
||||
|
||||
export function View(props: ViewProps) { |
||||
const { style, lightColor, darkColor, ...otherProps } = props; |
||||
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); |
||||
|
||||
return <DefaultView style={[{ backgroundColor }, style]} {...otherProps} />; |
||||
} |
@ -0,0 +1,10 @@ |
||||
import * as React from 'react'; |
||||
import renderer from 'react-test-renderer'; |
||||
|
||||
import { MonoText } from '../StyledText'; |
||||
|
||||
it(`renders correctly`, () => { |
||||
const tree = renderer.create(<MonoText>Snapshot test!</MonoText>).toJSON(); |
||||
|
||||
expect(tree).toMatchSnapshot(); |
||||
}); |
@ -0,0 +1,4 @@ |
||||
// This function is web-only as native doesn't currently support server (or build-time) rendering.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C { |
||||
return client; |
||||
} |
@ -0,0 +1,12 @@ |
||||
import React from 'react'; |
||||
|
||||
// `useEffect` is not invoked during server rendering, meaning
|
||||
// we can use this to determine if we're on the server or not.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C { |
||||
const [value, setValue] = React.useState<S | C>(server); |
||||
React.useEffect(() => { |
||||
setValue(client); |
||||
}, [client]); |
||||
|
||||
return value; |
||||
} |
@ -0,0 +1 @@ |
||||
export { useColorScheme } from 'react-native'; |
@ -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'; |
||||
} |
@ -0,0 +1,19 @@ |
||||
const tintColorLight = '#2f95dc'; |
||||
const tintColorDark = '#fff'; |
||||
|
||||
export default { |
||||
light: { |
||||
text: '#000', |
||||
background: '#fff', |
||||
tint: tintColorLight, |
||||
tabIconDefault: '#ccc', |
||||
tabIconSelected: tintColorLight, |
||||
}, |
||||
dark: { |
||||
text: '#fff', |
||||
background: '#000', |
||||
tint: tintColorDark, |
||||
tabIconDefault: '#ccc', |
||||
tabIconSelected: tintColorDark, |
||||
}, |
||||
}; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@ |
||||
{ |
||||
"name": "my-expo-sample", |
||||
"main": "expo-router/entry", |
||||
"version": "1.0.0", |
||||
"scripts": { |
||||
"start": "expo start", |
||||
"android": "expo start --android", |
||||
"ios": "expo start --ios", |
||||
"web": "expo start --web", |
||||
"test": "jest --watchAll" |
||||
}, |
||||
"jest": { |
||||
"preset": "jest-expo" |
||||
}, |
||||
"dependencies": { |
||||
"@expo/vector-icons": "^14.1.0", |
||||
"@react-navigation/native": "^7.1.6", |
||||
"expo": "~53.0.15", |
||||
"expo-font": "~13.3.2", |
||||
"expo-linking": "~7.1.6", |
||||
"expo-router": "~5.1.2", |
||||
"expo-splash-screen": "~0.30.9", |
||||
"expo-status-bar": "~2.2.3", |
||||
"expo-system-ui": "~5.0.9", |
||||
"expo-web-browser": "~14.2.0", |
||||
"react": "19.0.0", |
||||
"react-dom": "19.0.0", |
||||
"react-native": "0.79.4", |
||||
"react-native-reanimated": "~3.17.4", |
||||
"react-native-safe-area-context": "5.4.0", |
||||
"react-native-screens": "~4.11.1", |
||||
"react-native-web": "~0.20.0" |
||||
}, |
||||
"devDependencies": { |
||||
"@babel/core": "^7.25.2", |
||||
"@types/react": "~19.0.10", |
||||
"jest": "^29.2.1", |
||||
"jest-expo": "~53.0.8", |
||||
"react-test-renderer": "19.0.0", |
||||
"typescript": "~5.8.3" |
||||
}, |
||||
"private": true |
||||
} |
@ -0,0 +1,17 @@ |
||||
{ |
||||
"extends": "expo/tsconfig.base", |
||||
"compilerOptions": { |
||||
"strict": true, |
||||
"paths": { |
||||
"@/*": [ |
||||
"./*" |
||||
] |
||||
} |
||||
}, |
||||
"include": [ |
||||
"**/*.ts", |
||||
"**/*.tsx", |
||||
".expo/types/**/*.ts", |
||||
"expo-env.d.ts" |
||||
] |
||||
} |
Loading…
Reference in new issue