From d1a8dbc9d291e791e45608c0a6e12a23e66700f6 Mon Sep 17 00:00:00 2001 From: Stephen Hanson Date: Wed, 21 Aug 2024 09:46:36 -0500 Subject: [PATCH 1/2] Show GitHub projects on new About tab --- CONTRIBUTING.md | 16 +++ templates/boilerplate/package.json | 1 + .../src/components/ExampleCoffees.tsx | 49 ------- .../src/components/HeaderShadow.tsx | 51 +++++++ .../boilerplate/src/components/Screen.tsx | 110 ++++++++++++++++ .../boilerplate/src/navigators/AboutStack.tsx | 14 ++ .../src/navigators/DashboardStack.tsx | 2 +- .../src/navigators/TabNavigator.tsx | 22 +++- .../src/navigators/navigatorTypes.tsx | 25 +++- .../src/screens/AboutScreen/AboutScreen.tsx | 124 ++++++++++++++++++ .../src/screens/HomeScreen/HomeScreen.tsx | 6 +- .../InformationScreen/InformationScreen.tsx | 3 +- templates/boilerplate/src/util/api/api.ts | 19 ++- 13 files changed, 378 insertions(+), 64 deletions(-) delete mode 100644 templates/boilerplate/src/components/ExampleCoffees.tsx create mode 100644 templates/boilerplate/src/components/HeaderShadow.tsx create mode 100644 templates/boilerplate/src/components/Screen.tsx create mode 100644 templates/boilerplate/src/navigators/AboutStack.tsx create mode 100644 templates/boilerplate/src/screens/AboutScreen/AboutScreen.tsx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 94dfc53..7097390 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,6 +44,22 @@ bun bin/belt.js MyApp This generates a new Belt app in builds/MyApp, `cd`s to the directory, runs tests, and then `cd`s back. You can then run that app by `cd`ing to the directory and running `yarn ios` or the desired command. +## Common development techniques + +One way to build new features is to generate a new Belt app locally using the command outlined above. You can then build the new feature in the generated app and then copy the changed files back over to Belt. Example: + +``` +> bun bin/belt.js MyApp +> cd builds/MyApp +# now make some changes + +# now copy changes back into Belt, dry-run first (-n flag): +> rsync -avpn . ../../templates/boilerplate/ --exclude node_modules --exclude .cache --exclude .expo --exclude .vscode --exclude assets --exclude .git --exclude .gitignore + +# now run without the dry-run flag: +> rsync -avp . ../../templates/boilerplate/ --exclude node_modules --exclude .cache --exclude .expo --exclude .vscode --exclude assets --exclude .git --exclude .gitignore +``` + ## Creating a pull request Make sure the tests pass: diff --git a/templates/boilerplate/package.json b/templates/boilerplate/package.json index a0ed68a..24e5461 100644 --- a/templates/boilerplate/package.json +++ b/templates/boilerplate/package.json @@ -30,6 +30,7 @@ "msw": "^2.2.14", "react": "18.2.0", "react-native": "0.74.5", + "react-native-keyboard-aware-scroll-view": "^0.9.5", "react-native-keyboard-aware-scrollview": "^2.1.0", "react-native-safe-area-context": "4.10.5", "react-native-screens": "3.31.1" diff --git a/templates/boilerplate/src/components/ExampleCoffees.tsx b/templates/boilerplate/src/components/ExampleCoffees.tsx deleted file mode 100644 index bf1a228..0000000 --- a/templates/boilerplate/src/components/ExampleCoffees.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { FlatList, Image, Text, View } from 'react-native'; -import api, { Coffee as CoffeeType } from 'src/util/api/api'; - -// TODO: sample data, remove -export default function ExampleCoffees() { - const { data } = useQuery({ queryKey: ['coffee'], queryFn: api.coffee }); - - return ( - <> - Coffees - } - keyExtractor={(item) => item.id.toString()} - style={{ flexGrow: 0 }} - /> - - ); -} - -function Coffee({ coffee }: { coffee: CoffeeType }) { - const { title, image } = coffee; - - return ( - - - {title} - - - - ); -} diff --git a/templates/boilerplate/src/components/HeaderShadow.tsx b/templates/boilerplate/src/components/HeaderShadow.tsx new file mode 100644 index 0000000..55e0c90 --- /dev/null +++ b/templates/boilerplate/src/components/HeaderShadow.tsx @@ -0,0 +1,51 @@ +import { Animated, StyleSheet } from 'react-native'; + +type Props = { + animatedScrollOffset: Animated.Value; + fixedTop?: boolean; + animationScrollDistance?: number; +}; + +const SHADOW_HEIGHT = 12; // Height of the element that creates the shadow. Will never be seen. + +export default function HeaderShadow({ + animatedScrollOffset = new Animated.Value(0), + fixedTop = false, + animationScrollDistance = 32, +}: Props) { + const shadowOpacity = animatedScrollOffset.interpolate({ + inputRange: [0, animationScrollDistance], + outputRange: [0, 0.4], // Change of opacity of the shadow by changing the opacity of the entire component + extrapolate: 'clamp', + }); + + return ( + + ); +} + +const styles = StyleSheet.create({ + shadow: { + // Shadow is applied to its own element so it can be faded in and out + // with opacity which is more performant to animate + position: 'absolute', + left: 0, + right: 0, + elevation: 10, + shadowColor: '#000', + shadowRadius: 5.46, + }, +}); diff --git a/templates/boilerplate/src/components/Screen.tsx b/templates/boilerplate/src/components/Screen.tsx new file mode 100644 index 0000000..db32dcf --- /dev/null +++ b/templates/boilerplate/src/components/Screen.tsx @@ -0,0 +1,110 @@ +import { useNavigation } from '@react-navigation/native'; +import { StatusBarStyle } from 'expo-status-bar'; +import { ReactNode } from 'react'; +import { Animated, StyleSheet, View } from 'react-native'; +import { + KeyboardAwareScrollView, + KeyboardAwareScrollViewProps, +} from 'react-native-keyboard-aware-scroll-view'; +import { + SafeAreaView, + useSafeAreaInsets, +} from 'react-native-safe-area-context'; + +type Props = KeyboardAwareScrollViewProps & { + padHorizontal?: boolean; + scroll?: boolean; + /** + * If true, a safe area view is not added for the top of the screen, since it is + * handled instead by React Navigation + */ + hasHeader?: boolean; + /** A React component to render fixed to the bottom of the screen. It is not + * positioned absolutely and would show above a tab bar. If your screen does + * not have a tab bar, set fixedBottomAddSafeArea to ensure a safe area view + * is used on the bottom */ + FixedBottomComponent?: ReactNode; + fixedBottomAddSafeArea?: boolean; + statusBarStyle?: StatusBarStyle; +}; + +const AnimatedKeyboardAwareScrollView = Animated.createAnimatedComponent( + KeyboardAwareScrollView, +); + +export default function Screen({ + style, + padHorizontal = true, + scroll = true, + testID, + /** if screen has a navigation header, safe area view is not needed, since header takes into account */ + hasHeader = false, + children, + FixedBottomComponent, + fixedBottomAddSafeArea = false, + ...props +}: Props) { + const navigation = useNavigation(); + const insets = useSafeAreaInsets(); + + return ( + + + {scroll ? ( + navigation.goBack()} + testID={`${testID || 'ScreenContainer'}ScrollView`} + showsVerticalScrollIndicator={false} + contentContainerStyle={{ + paddingTop: 0, + }} + {...props} + > + {children} + + ) : ( + children + )} + + {!!FixedBottomComponent && ( + + {FixedBottomComponent} + + )} + + ); +} + +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + }, + contentContainer: { + flex: 1, + }, + horizontalPadding: { + paddingHorizontal: 20, + }, +}); diff --git a/templates/boilerplate/src/navigators/AboutStack.tsx b/templates/boilerplate/src/navigators/AboutStack.tsx new file mode 100644 index 0000000..c41fa52 --- /dev/null +++ b/templates/boilerplate/src/navigators/AboutStack.tsx @@ -0,0 +1,14 @@ +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import React from 'react'; +import AboutScreen from 'src/screens/AboutScreen/AboutScreen'; +import { AboutTabParamList } from './navigatorTypes'; + +const About = createNativeStackNavigator(); + +export default function AboutStack() { + return ( + + + + ); +} diff --git a/templates/boilerplate/src/navigators/DashboardStack.tsx b/templates/boilerplate/src/navigators/DashboardStack.tsx index 610788b..5de7e34 100644 --- a/templates/boilerplate/src/navigators/DashboardStack.tsx +++ b/templates/boilerplate/src/navigators/DashboardStack.tsx @@ -1,6 +1,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React from 'react'; -import HomeScreen from 'src/screens/HomeScreen/HomeScreen'; +import HomeScreen from '../screens/HomeScreen/HomeScreen'; import InformationScreen from '../screens/InformationScreen/InformationScreen'; import { DashboardTabParamList } from './navigatorTypes'; diff --git a/templates/boilerplate/src/navigators/TabNavigator.tsx b/templates/boilerplate/src/navigators/TabNavigator.tsx index 0ee3bb5..09c3f20 100644 --- a/templates/boilerplate/src/navigators/TabNavigator.tsx +++ b/templates/boilerplate/src/navigators/TabNavigator.tsx @@ -1,19 +1,22 @@ -import { MaterialCommunityIcons } from '@expo/vector-icons'; +import Feather from '@expo/vector-icons/Feather'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import SettingsScreen from '../screens/SettingsScreen/SettingsScreen'; +import AboutStack from './AboutStack'; import DashboardStack from './DashboardStack'; import { TabsParamList } from './navigatorTypes'; const Tab = createBottomTabNavigator(); function HomeIcon({ focused = false, color = 'gray' }) { - return ; + return ; +} + +function AboutIcon({ focused = false, color = 'gray' }) { + return ; } function AccountIcon({ focused = false, color = 'gray' }) { - return ( - - ); + return ; } export default function TabNavigator() { @@ -38,6 +41,15 @@ export default function TabNavigator() { tabBarLabel: 'Home', }} /> + ; }; +// Each tab goes here export type TabsParamList = { DashboardTab: NavigatorScreenParams; SettingsTab: NavigatorScreenParams; + AboutTab: NavigatorScreenParams; }; +/* ---------------------------------------------------------------- + For each tab, define all of the screens that can be navigated to + -------------------------------------------------------------*/ + export type DashboardTabParamList = { Home: undefined; - Information: { owner: string } | undefined; + Information: { greeting: string } | undefined; +}; + +export type AboutTabParamList = { + About: undefined; }; export type SettingsTabParamList = { @@ -29,11 +39,24 @@ export type AppRouteName = | keyof DashboardTabParamList | keyof SettingsTabParamList; +/* ---------------------------------------------------------------- + Define ScreenProp type for each screen that might need to access + navigation or route props + Usage eg: + const navigation = useNavigation(); + const params = useRoute(); + -------------------------------------------------------------*/ + export type HomeScreenProp = NativeStackScreenProps< DashboardTabParamList, 'Home' >; +export type AboutScreenProp = NativeStackScreenProps< + AboutTabParamList, + 'About' +>; + export type InformationScreenProp = NativeStackScreenProps< DashboardTabParamList, 'Information' diff --git a/templates/boilerplate/src/screens/AboutScreen/AboutScreen.tsx b/templates/boilerplate/src/screens/AboutScreen/AboutScreen.tsx new file mode 100644 index 0000000..0182ba3 --- /dev/null +++ b/templates/boilerplate/src/screens/AboutScreen/AboutScreen.tsx @@ -0,0 +1,124 @@ +import Feather from '@expo/vector-icons/Feather'; +import { useQuery } from '@tanstack/react-query'; +import { + ActivityIndicator, + FlatList, + StyleSheet, + Text, + View, +} from 'react-native'; +import Screen from 'src/components/Screen'; +import api, { GithubProject } from 'src/util/api/api'; + +export default function AboutScreen() { + const { data, isLoading } = useQuery({ + queryKey: ['githubRepos'], + queryFn: api.githubRepos, + }); + + const projects = data?.projects + ? data.projects.sort((a, b) => (b.stars || 0) - (a.stars || 0)).slice(0, 30) + : []; + + return ( + + } + keyExtractor={(item) => item.id} + ListHeaderComponent={Header} + stickyHeaderHiddenOnScroll + ListEmptyComponent={ + isLoading ? : No results found + } + style={{ flexGrow: 0 }} + /> + + ); +} + +function Header() { + return ( + <> + Open Source + + Here are a few projects that we maintain. + + + ); +} + +// TODO: sample data, remove +function Project({ project }: { project: GithubProject }) { + const { name, description, stars } = project; + const formatStars = () => { + if (!stars || stars < 1000) { + return stars; + } + + return `${(stars / 1000).toFixed(1)}k`; + }; + const formattedStars = formatStars(); + + return ( + + + + {name} + + + {formattedStars != null && ( + + + {formatStars()} + + )} + + + {description} + + ); +} + +const styles = StyleSheet.create({ + projectHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + starContainer: { + flexDirection: 'row', + gap: 6, + }, + heading: { + fontWeight: '800', + fontSize: 24, + marginTop: 48, + marginBottom: 12, + paddingHorizontal: 16, + }, + paragraph: { + fontSize: 16, + marginBottom: 16, + paddingHorizontal: 16, + }, + project: { + paddingHorizontal: 16, + paddingVertical: 20, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: 'rgba(0, 0, 0, 0.2)', + }, + projectName: { + fontWeight: '500', + fontSize: 18, + marginBottom: 4, + }, + projectDescription: { + fontSize: 15, + lineHeight: 22, + }, + projectStars: { + // + }, +}); diff --git a/templates/boilerplate/src/screens/HomeScreen/HomeScreen.tsx b/templates/boilerplate/src/screens/HomeScreen/HomeScreen.tsx index ae7b666..c899a32 100644 --- a/templates/boilerplate/src/screens/HomeScreen/HomeScreen.tsx +++ b/templates/boilerplate/src/screens/HomeScreen/HomeScreen.tsx @@ -1,7 +1,6 @@ import { useNavigation } from '@react-navigation/native'; import { StatusBar } from 'expo-status-bar'; import { Button, StyleSheet, Text, View } from 'react-native'; -import ExampleCoffees from 'src/components/ExampleCoffees'; import { HomeScreenProp } from 'src/navigators/navigatorTypes'; export default function HomeScreen() { @@ -12,10 +11,11 @@ export default function HomeScreen() { Open up App.tsx to start working on your app!