diff options
author | Santo Cariotti <santo@dcariotti.me> | 2024-08-28 15:53:21 +0200 |
---|---|---|
committer | Santo Cariotti <santo@dcariotti.me> | 2024-08-28 15:53:21 +0200 |
commit | 83643a78b73dee5610be6ad9837fb72e9b944cb7 (patch) | |
tree | 1eca6bad452656f78879c829181362f3b586d697 /components |
Initial commit
Generated by create-expo-app 3.0.0.
Diffstat (limited to 'components')
-rw-r--r-- | components/Collapsible.tsx | 41 | ||||
-rw-r--r-- | components/ExternalLink.tsx | 24 | ||||
-rw-r--r-- | components/HelloWave.tsx | 37 | ||||
-rw-r--r-- | components/ParallaxScrollView.tsx | 76 | ||||
-rw-r--r-- | components/ThemedText.tsx | 60 | ||||
-rw-r--r-- | components/ThemedView.tsx | 14 | ||||
-rw-r--r-- | components/__tests__/ThemedText-test.tsx | 10 | ||||
-rw-r--r-- | components/__tests__/__snapshots__/ThemedText-test.tsx.snap | 24 | ||||
-rw-r--r-- | components/navigation/TabBarIcon.tsx | 9 |
9 files changed, 295 insertions, 0 deletions
diff --git a/components/Collapsible.tsx b/components/Collapsible.tsx new file mode 100644 index 0000000..c326473 --- /dev/null +++ b/components/Collapsible.tsx @@ -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, + }, +}); diff --git a/components/ExternalLink.tsx b/components/ExternalLink.tsx new file mode 100644 index 0000000..8f05675 --- /dev/null +++ b/components/ExternalLink.tsx @@ -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); + } + }} + /> + ); +} diff --git a/components/HelloWave.tsx b/components/HelloWave.tsx new file mode 100644 index 0000000..f4b6ea5 --- /dev/null +++ b/components/HelloWave.tsx @@ -0,0 +1,37 @@ +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, + }, +}); diff --git a/components/ParallaxScrollView.tsx b/components/ParallaxScrollView.tsx new file mode 100644 index 0000000..0a35419 --- /dev/null +++ b/components/ParallaxScrollView.tsx @@ -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', + }, +}); diff --git a/components/ThemedText.tsx b/components/ThemedText.tsx new file mode 100644 index 0000000..c0e1a78 --- /dev/null +++ b/components/ThemedText.tsx @@ -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', + }, +}); diff --git a/components/ThemedView.tsx b/components/ThemedView.tsx new file mode 100644 index 0000000..4d2cb09 --- /dev/null +++ b/components/ThemedView.tsx @@ -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} />; +} diff --git a/components/__tests__/ThemedText-test.tsx b/components/__tests__/ThemedText-test.tsx new file mode 100644 index 0000000..1ac3225 --- /dev/null +++ b/components/__tests__/ThemedText-test.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import renderer from 'react-test-renderer'; + +import { ThemedText } from '../ThemedText'; + +it(`renders correctly`, () => { + const tree = renderer.create(<ThemedText>Snapshot test!</ThemedText>).toJSON(); + + expect(tree).toMatchSnapshot(); +}); diff --git a/components/__tests__/__snapshots__/ThemedText-test.tsx.snap b/components/__tests__/__snapshots__/ThemedText-test.tsx.snap new file mode 100644 index 0000000..b68e53e --- /dev/null +++ b/components/__tests__/__snapshots__/ThemedText-test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<Text + style={ + [ + { + "color": "#11181C", + }, + { + "fontSize": 16, + "lineHeight": 24, + }, + undefined, + undefined, + undefined, + undefined, + undefined, + ] + } +> + Snapshot test! +</Text> +`; diff --git a/components/navigation/TabBarIcon.tsx b/components/navigation/TabBarIcon.tsx new file mode 100644 index 0000000..b7302c3 --- /dev/null +++ b/components/navigation/TabBarIcon.tsx @@ -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} />; +} |