├── .gitignore ├── App.tsx ├── app.json ├── assets ├── data │ ├── event.json │ ├── events.json │ └── users.json ├── fonts │ └── SpaceMono-Regular.ttf └── images │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── components ├── CustomButton │ ├── CustomButton.tsx │ └── index.ts ├── EditScreenInfo.tsx ├── StyledText.tsx ├── Themed.tsx ├── UserListItem.tsx └── __tests__ │ └── StyledText-test.js ├── constants ├── Colors.ts └── Layout.ts ├── context └── ChatContext.tsx ├── hooks ├── useCachedResources.ts └── useColorScheme.ts ├── navigation ├── ChatStackNavigator.tsx ├── LinkingConfiguration.ts └── index.tsx ├── package-lock.json ├── package.json ├── screens ├── AuthScreens │ ├── SignInScreen │ │ ├── SignInScreen.tsx │ │ ├── index.ts │ │ └── logo.png │ ├── SignUpScreen │ │ ├── SignUpScreen.tsx │ │ └── index.ts │ └── components │ │ ├── CustomButton │ │ ├── CustomButton.tsx │ │ └── index.ts │ │ ├── CustomInput │ │ ├── CustomInput.tsx │ │ └── index.ts │ │ └── SocialSignInButtons │ │ ├── SocialSignInButtons.tsx │ │ └── index.ts ├── Chat │ ├── ChatRoomScreen.tsx │ └── ChatsScreen.tsx ├── ModalScreen.tsx ├── NotFoundScreen.tsx ├── TabOneScreen.tsx ├── TabTwoScreen.tsx └── UsersScreen.tsx ├── tsconfig.json └── types.tsx /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import "react-native-gesture-handler"; 2 | import { StatusBar } from "expo-status-bar"; 3 | import { SafeAreaProvider } from "react-native-safe-area-context"; 4 | 5 | import useCachedResources from "./hooks/useCachedResources"; 6 | import useColorScheme from "./hooks/useColorScheme"; 7 | import Navigation from "./navigation"; 8 | 9 | import { NhostClient, NhostReactProvider } from "@nhost/react"; 10 | import { NhostApolloProvider } from "@nhost/react-apollo"; 11 | import * as SecureStore from "expo-secure-store"; 12 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 13 | 14 | const nhost = new NhostClient({ 15 | subdomain: "mnjdtroppbmdosupvetf", 16 | region: "eu-central-1", 17 | clientStorageType: "expo-secure-storage", 18 | clientStorage: SecureStore, 19 | }); 20 | 21 | export default function App() { 22 | const isLoadingComplete = useCachedResources(); 23 | const colorScheme = useColorScheme(); 24 | 25 | if (!isLoadingComplete) { 26 | return null; 27 | } else { 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "VirtialEvents", 4 | "slug": "VirtialEvents", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "updates": { 16 | "fallbackToCacheTimeout": 0 17 | }, 18 | "assetBundlePatterns": [ 19 | "**/*" 20 | ], 21 | "ios": { 22 | "supportsTablet": true 23 | }, 24 | "android": { 25 | "adaptiveIcon": { 26 | "foregroundImage": "./assets/images/adaptive-icon.png", 27 | "backgroundColor": "#ffffff" 28 | } 29 | }, 30 | "web": { 31 | "favicon": "./assets/images/favicon.png" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /assets/data/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1", 3 | "name": "Live: notJust.Hack Kickstart", 4 | // "height": 50, 5 | // "day": "2022-11-24", 6 | "date": "2022-11-24T03:00:00+00:00" 7 | } 8 | -------------------------------------------------------------------------------- /assets/data/events.json: -------------------------------------------------------------------------------- 1 | { 2 | "2022-11-23": [], 3 | "2022-11-24": [ 4 | { 5 | "id": "1", 6 | "name": "Live: notJust.Hack Kickstart", 7 | "height": 50, 8 | "day": "2022-11-24" 9 | } 10 | ], 11 | "2022-11-25": [ 12 | { 13 | "id": "2", 14 | "name": "Workshop: Build any mobile application with React Native", 15 | "height": 50, 16 | "day": "2022-11-25" 17 | }, 18 | { 19 | "id": "3", 20 | "name": "Q&A session", 21 | "height": 50, 22 | "day": "2022-11-25" 23 | } 24 | ], 25 | "2022-11-26": [ 26 | { 27 | "id": "4", 28 | "name": "Workshop: Build a Chat application in hours using Stream", 29 | "height": 50, 30 | "day": "2022-11-26" 31 | }, 32 | { 33 | "id": "5", 34 | "name": "Q&A session", 35 | "height": 50, 36 | "day": "2022-11-26" 37 | } 38 | ], 39 | "2022-11-27": [ 40 | { 41 | "id": "6", 42 | "name": "Workshop: Build Full-Stack applications with Nhost", 43 | "height": 50, 44 | "day": "2022-11-27" 45 | }, 46 | { 47 | "id": "7", 48 | "name": "Q&A session", 49 | "height": 50, 50 | "day": "2022-11-27" 51 | } 52 | ], 53 | "2022-11-28": [ 54 | { 55 | "id": "8", 56 | "name": "Demo Day", 57 | "height": 50, 58 | "day": "2022-11-28" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /assets/data/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "u1", 4 | "avatarUrl": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg", 5 | "displayName": "Vadim Savin" 6 | }, 7 | { 8 | "id": "u2", 9 | "avatarUrl": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/elon.png", 10 | "displayName": "Elon Musk" 11 | }, 12 | { 13 | "id": "u3", 14 | "avatarUrl": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/jeff.jpeg", 15 | "displayName": "Jeff" 16 | }, 17 | { 18 | "id": "u4", 19 | "avatarUrl": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/zuck.jpeg", 20 | "displayName": "Zuck" 21 | }, 22 | { 23 | "id": "u5", 24 | "avatarUrl": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/6.png", 25 | "displayName": "Alexandra" 26 | }, 27 | { 28 | "id": "u6", 29 | "avatarUrl": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/biahaze.jpg", 30 | "displayName": "Lukas" 31 | }, 32 | { 33 | "id": "u7", 34 | "avatarUrl": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/graham.jpg", 35 | "displayName": "Daniil PR" 36 | }, 37 | { 38 | "id": "u8", 39 | "avatarUrl": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/11.png", 40 | "displayName": "Catalin Editor" 41 | }, 42 | { 43 | "id": "u9", 44 | "avatarUrl": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/10.png", 45 | "displayName": "Andrei Developer" 46 | }, 47 | { 48 | "id": "u10", 49 | "avatarUrl": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/7.png", 50 | "displayName": "Marry HR" 51 | }, 52 | { 53 | "id": "u11", 54 | "avatarUrl": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/2.jpg", 55 | "displayName": "Jhon Marketing" 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/VirtualEvents/56f820028a88ed1a7ef8455fa07eb0b992bbff1c/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/VirtualEvents/56f820028a88ed1a7ef8455fa07eb0b992bbff1c/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/VirtualEvents/56f820028a88ed1a7ef8455fa07eb0b992bbff1c/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/VirtualEvents/56f820028a88ed1a7ef8455fa07eb0b992bbff1c/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/VirtualEvents/56f820028a88ed1a7ef8455fa07eb0b992bbff1c/assets/images/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: ["react-native-reanimated/plugin"], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /components/CustomButton/CustomButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, StyleSheet, Pressable } from "react-native"; 3 | 4 | type CustomButtonProps = { 5 | onPress: () => void; 6 | text: string; 7 | type?: "PRIMARY" | "SECONDARY" | "TERTIARY"; 8 | bgColor?: string; 9 | fgColor?: string; 10 | }; 11 | 12 | const CustomButton = ({ 13 | onPress, 14 | text, 15 | type = "PRIMARY", 16 | bgColor, 17 | fgColor, 18 | }: CustomButtonProps) => { 19 | return ( 20 | 28 | 35 | {text} 36 | 37 | 38 | ); 39 | }; 40 | 41 | const styles = StyleSheet.create({ 42 | container: { 43 | width: "100%", 44 | 45 | padding: 15, 46 | marginVertical: 5, 47 | 48 | alignItems: "center", 49 | borderRadius: 5, 50 | }, 51 | 52 | container_PRIMARY: { 53 | backgroundColor: "#3B71F3", 54 | }, 55 | 56 | container_SECONDARY: { 57 | borderColor: "#3B71F3", 58 | borderWidth: 2, 59 | }, 60 | 61 | container_TERTIARY: {}, 62 | 63 | text: { 64 | fontWeight: "bold", 65 | color: "white", 66 | }, 67 | 68 | text_PRIMARY: {}, 69 | 70 | text_SECONDARY: { 71 | color: "#3B71F3", 72 | }, 73 | 74 | text_TERTIARY: { 75 | color: "gray", 76 | }, 77 | }); 78 | 79 | export default CustomButton; 80 | -------------------------------------------------------------------------------- /components/CustomButton/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './CustomButton'; 2 | -------------------------------------------------------------------------------- /components/EditScreenInfo.tsx: -------------------------------------------------------------------------------- 1 | import * as WebBrowser from 'expo-web-browser'; 2 | import { StyleSheet, TouchableOpacity } from 'react-native'; 3 | 4 | import Colors from '../constants/Colors'; 5 | import { MonoText } from './StyledText'; 6 | import { Text, View } from './Themed'; 7 | 8 | export default function EditScreenInfo({ path }: { path: string }) { 9 | return ( 10 | 11 | 12 | 16 | Open up the code for this screen: 17 | 18 | 19 | 23 | {path} 24 | 25 | 26 | 30 | Change any of the text, save the file, and your app will automatically update. 31 | 32 | 33 | 34 | 35 | 36 | 37 | Tap here if your app doesn't automatically update after making changes 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | function handleHelpPress() { 46 | WebBrowser.openBrowserAsync( 47 | 'https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet' 48 | ); 49 | } 50 | 51 | const styles = StyleSheet.create({ 52 | getStartedContainer: { 53 | alignItems: 'center', 54 | marginHorizontal: 50, 55 | }, 56 | homeScreenFilename: { 57 | marginVertical: 7, 58 | }, 59 | codeHighlightContainer: { 60 | borderRadius: 3, 61 | paddingHorizontal: 4, 62 | }, 63 | getStartedText: { 64 | fontSize: 17, 65 | lineHeight: 24, 66 | textAlign: 'center', 67 | }, 68 | helpContainer: { 69 | marginTop: 15, 70 | marginHorizontal: 20, 71 | alignItems: 'center', 72 | }, 73 | helpLink: { 74 | paddingVertical: 15, 75 | }, 76 | helpLinkText: { 77 | textAlign: 'center', 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /components/StyledText.tsx: -------------------------------------------------------------------------------- 1 | import { Text, TextProps } from './Themed'; 2 | 3 | export function MonoText(props: TextProps) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /components/Themed.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about Light and Dark modes: 3 | * https://docs.expo.io/guides/color-schemes/ 4 | */ 5 | 6 | import { Text as DefaultText, View as DefaultView } from 'react-native'; 7 | 8 | import Colors from '../constants/Colors'; 9 | import useColorScheme from '../hooks/useColorScheme'; 10 | 11 | export function useThemeColor( 12 | props: { light?: string; dark?: string }, 13 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark 14 | ) { 15 | const theme = useColorScheme(); 16 | const colorFromProps = props[theme]; 17 | 18 | if (colorFromProps) { 19 | return colorFromProps; 20 | } else { 21 | return Colors[theme][colorName]; 22 | } 23 | } 24 | 25 | type ThemeProps = { 26 | lightColor?: string; 27 | darkColor?: string; 28 | }; 29 | 30 | export type TextProps = ThemeProps & DefaultText['props']; 31 | export type ViewProps = ThemeProps & DefaultView['props']; 32 | 33 | export function Text(props: TextProps) { 34 | const { style, lightColor, darkColor, ...otherProps } = props; 35 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); 36 | 37 | return ; 38 | } 39 | 40 | export function View(props: ViewProps) { 41 | const { style, lightColor, darkColor, ...otherProps } = props; 42 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); 43 | 44 | return ; 45 | } 46 | -------------------------------------------------------------------------------- /components/UserListItem.tsx: -------------------------------------------------------------------------------- 1 | import { Pressable, Text, Image, StyleSheet } from "react-native"; 2 | import React from "react"; 3 | import { useChatContext } from "../context/ChatContext"; 4 | 5 | type UserListItemProps = { 6 | user: any; 7 | }; 8 | 9 | const UserListItem = ({ user }: UserListItemProps) => { 10 | const { startDMChatRoom } = useChatContext(); 11 | 12 | return ( 13 | startDMChatRoom(user)} style={styles.container}> 14 | 15 | {user.displayName} 16 | 17 | ); 18 | }; 19 | 20 | const styles = StyleSheet.create({ 21 | container: { 22 | flexDirection: "row", 23 | alignItems: "center", 24 | padding: 5, 25 | margin: 5, 26 | marginHorizontal: 10, 27 | backgroundColor: "white", 28 | borderRadius: 5, 29 | }, 30 | image: { 31 | width: 50, 32 | aspectRatio: 1, 33 | borderRadius: 50, 34 | }, 35 | name: { 36 | fontWeight: "bold", 37 | marginLeft: 10, 38 | }, 39 | }); 40 | 41 | export default UserListItem; 42 | -------------------------------------------------------------------------------- /components/__tests__/StyledText-test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import { MonoText } from '../StyledText'; 5 | 6 | it(`renders correctly`, () => { 7 | const tree = renderer.create(Snapshot test!).toJSON(); 8 | 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /constants/Colors.ts: -------------------------------------------------------------------------------- 1 | const tintColorLight = '#2f95dc'; 2 | const tintColorDark = '#fff'; 3 | 4 | export default { 5 | light: { 6 | text: '#000', 7 | background: '#fff', 8 | tint: tintColorLight, 9 | tabIconDefault: '#ccc', 10 | tabIconSelected: tintColorLight, 11 | }, 12 | dark: { 13 | text: '#fff', 14 | background: '#000', 15 | tint: tintColorDark, 16 | tabIconDefault: '#ccc', 17 | tabIconSelected: tintColorDark, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /constants/Layout.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | 3 | const width = Dimensions.get('window').width; 4 | const height = Dimensions.get('window').height; 5 | 6 | export default { 7 | window: { 8 | width, 9 | height, 10 | }, 11 | isSmallDevice: width < 375, 12 | }; 13 | -------------------------------------------------------------------------------- /context/ChatContext.tsx: -------------------------------------------------------------------------------- 1 | import { useUserData } from "@nhost/react"; 2 | import { useNavigation } from "@react-navigation/native"; 3 | import React, { createContext, useContext, useEffect, useState } from "react"; 4 | import { ActivityIndicator } from "react-native"; 5 | import { StreamChat, Channel } from "stream-chat"; 6 | import { OverlayProvider, Chat } from "stream-chat-expo"; 7 | 8 | type ChatContextType = { 9 | currentChannel?: Channel; 10 | }; 11 | 12 | export const ChatContext = createContext({ 13 | currentChannel: undefined, 14 | }); 15 | 16 | const ChatContextProvider = ({ children }: { children: React.ReactNode }) => { 17 | // component 18 | const [chatClient, setChatClient] = useState(); 19 | const [currentChannel, setCurrentChannel] = useState(); 20 | const user = useUserData(); 21 | const navigation = useNavigation(); 22 | 23 | useEffect(() => { 24 | const initChat = async () => { 25 | if (!user) { 26 | return; 27 | } 28 | 29 | const client = StreamChat.getInstance("249ewtgkuz9h"); 30 | 31 | // get information about the authenticated 32 | // connect the user to stream chat 33 | await client.connectUser( 34 | { 35 | id: user.id, 36 | name: user.displayName, 37 | image: user.avatarUrl, 38 | }, 39 | client.devToken(user.id) 40 | ); 41 | 42 | setChatClient(client); 43 | 44 | const globalChannel = client.channel("livestream", "global", { 45 | name: "notJust.dev", 46 | }); 47 | 48 | await globalChannel.watch(); 49 | }; 50 | 51 | initChat(); 52 | }, []); 53 | 54 | useEffect(() => { 55 | return () => { 56 | if (chatClient) { 57 | chatClient.disconnectUser(); 58 | } 59 | }; 60 | }, []); 61 | 62 | const startDMChatRoom = async (chatWithUser) => { 63 | if (!chatClient) { 64 | return; 65 | } 66 | const newChannel = chatClient.channel("messaging", { 67 | members: [chatClient.userID, chatWithUser.id], 68 | }); 69 | 70 | await newChannel.watch(); 71 | setCurrentChannel(newChannel); 72 | 73 | // navigation.goBack(); 74 | navigation.replace("ChatRoom"); 75 | }; 76 | 77 | const joinEventChatRoom = async (event) => { 78 | if (!chatClient) { 79 | return; 80 | } 81 | const channelId = `room-${event.id}`; 82 | const eventChannel = chatClient.channel("livestream", channelId, { 83 | name: event.name, 84 | }); 85 | 86 | await eventChannel.watch({ watchers: { limit: 100 } }); 87 | setCurrentChannel(eventChannel); 88 | 89 | navigation.navigate("Root", { 90 | screen: "Chat", 91 | }); 92 | navigation.navigate("Root", { 93 | screen: "Chat", 94 | params: { screen: "ChatRoom" }, 95 | }); 96 | }; 97 | 98 | if (!chatClient) { 99 | return ; 100 | } 101 | 102 | const value = { 103 | chatClient, 104 | currentChannel, 105 | setCurrentChannel, 106 | startDMChatRoom, 107 | joinEventChatRoom, 108 | }; 109 | return ( 110 | 111 | 112 | {children} 113 | 114 | 115 | ); 116 | }; 117 | 118 | export const useChatContext = () => useContext(ChatContext); 119 | 120 | export default ChatContextProvider; 121 | -------------------------------------------------------------------------------- /hooks/useCachedResources.ts: -------------------------------------------------------------------------------- 1 | import { FontAwesome } from '@expo/vector-icons'; 2 | import * as Font from 'expo-font'; 3 | import * as SplashScreen from 'expo-splash-screen'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | export default function useCachedResources() { 7 | const [isLoadingComplete, setLoadingComplete] = useState(false); 8 | 9 | // Load any resources or data that we need prior to rendering the app 10 | useEffect(() => { 11 | async function loadResourcesAndDataAsync() { 12 | try { 13 | SplashScreen.preventAutoHideAsync(); 14 | 15 | // Load fonts 16 | await Font.loadAsync({ 17 | ...FontAwesome.font, 18 | 'space-mono': require('../assets/fonts/SpaceMono-Regular.ttf'), 19 | }); 20 | } catch (e) { 21 | // We might want to provide this error information to an error reporting service 22 | console.warn(e); 23 | } finally { 24 | setLoadingComplete(true); 25 | SplashScreen.hideAsync(); 26 | } 27 | } 28 | 29 | loadResourcesAndDataAsync(); 30 | }, []); 31 | 32 | return isLoadingComplete; 33 | } 34 | -------------------------------------------------------------------------------- /hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | import { ColorSchemeName, useColorScheme as _useColorScheme } from 'react-native'; 2 | 3 | // The useColorScheme value is always either light or dark, but the built-in 4 | // type suggests that it can be null. This will not happen in practice, so this 5 | // makes it a bit easier to work with. 6 | export default function useColorScheme(): NonNullable { 7 | return _useColorScheme() as NonNullable; 8 | } 9 | -------------------------------------------------------------------------------- /navigation/ChatStackNavigator.tsx: -------------------------------------------------------------------------------- 1 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 2 | import ChatsScreen from "../screens/Chat/ChatsScreen"; 3 | import ChatRoomScreen from "../screens/Chat/ChatRoomScreen"; 4 | import ChatContextProvider from "../context/ChatContext"; 5 | import UsersScreen from "../screens/UsersScreen"; 6 | import { Pressable } from "react-native"; 7 | import { FontAwesome } from "@expo/vector-icons"; 8 | 9 | const Stack = createNativeStackNavigator(); 10 | 11 | export default () => { 12 | return ( 13 | 14 | ({ 18 | headerRight: () => ( 19 | navigation.navigate("Users")} 21 | style={({ pressed }) => ({ 22 | opacity: pressed ? 0.5 : 1, 23 | })} 24 | > 25 | 31 | 32 | ), 33 | })} 34 | /> 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /navigation/LinkingConfiguration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about deep linking with React Navigation 3 | * https://reactnavigation.org/docs/deep-linking 4 | * https://reactnavigation.org/docs/configuring-links 5 | */ 6 | 7 | import { LinkingOptions } from '@react-navigation/native'; 8 | import * as Linking from 'expo-linking'; 9 | 10 | import { RootStackParamList } from '../types'; 11 | 12 | const linking: LinkingOptions = { 13 | prefixes: [Linking.createURL('/')], 14 | config: { 15 | screens: { 16 | Root: { 17 | screens: { 18 | TabOne: { 19 | screens: { 20 | TabOneScreen: 'one', 21 | }, 22 | }, 23 | TabTwo: { 24 | screens: { 25 | TabTwoScreen: 'two', 26 | }, 27 | }, 28 | }, 29 | }, 30 | Modal: 'modal', 31 | NotFound: '*', 32 | }, 33 | }, 34 | }; 35 | 36 | export default linking; 37 | -------------------------------------------------------------------------------- /navigation/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * If you are not familiar with React Navigation, refer to the "Fundamentals" guide: 3 | * https://reactnavigation.org/docs/getting-started 4 | * 5 | */ 6 | import { FontAwesome, Ionicons } from "@expo/vector-icons"; 7 | import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; 8 | import { 9 | NavigationContainer, 10 | DefaultTheme, 11 | DarkTheme, 12 | } from "@react-navigation/native"; 13 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 14 | import * as React from "react"; 15 | import { ActivityIndicator, ColorSchemeName, Pressable } from "react-native"; 16 | 17 | import Colors from "../constants/Colors"; 18 | import useColorScheme from "../hooks/useColorScheme"; 19 | import SignInScreen from "../screens/AuthScreens/SignInScreen"; 20 | import SignUpScreen from "../screens/AuthScreens/SignUpScreen"; 21 | import ModalScreen from "../screens/ModalScreen"; 22 | import NotFoundScreen from "../screens/NotFoundScreen"; 23 | import TabOneScreen from "../screens/TabOneScreen"; 24 | import TabTwoScreen from "../screens/TabTwoScreen"; 25 | import UsersScreen from "../screens/UsersScreen"; 26 | import { 27 | RootStackParamList, 28 | RootTabParamList, 29 | RootTabScreenProps, 30 | } from "../types"; 31 | import LinkingConfiguration from "./LinkingConfiguration"; 32 | import { useAuthenticationStatus } from "@nhost/react"; 33 | import ChatStackNavigator from "./ChatStackNavigator"; 34 | import ChatContextProvider from "../context/ChatContext"; 35 | 36 | export default function Navigation({ 37 | colorScheme, 38 | }: { 39 | colorScheme: ColorSchemeName; 40 | }) { 41 | return ( 42 | 46 | 47 | 48 | ); 49 | } 50 | 51 | /** 52 | * A root stack navigator is often used for displaying modals on top of all other content. 53 | * https://reactnavigation.org/docs/modal 54 | */ 55 | const Stack = createNativeStackNavigator(); 56 | 57 | function RootNavigator() { 58 | const { isAuthenticated, isLoading } = useAuthenticationStatus(); 59 | 60 | if (isLoading) { 61 | return ; 62 | } 63 | 64 | if (!isAuthenticated) { 65 | return ( 66 | 67 | 72 | 77 | 78 | ); 79 | } 80 | 81 | return ( 82 | 83 | 84 | 89 | 94 | 95 | 96 | 97 | 98 | 99 | ); 100 | } 101 | 102 | /** 103 | * A bottom tab navigator displays tab buttons on the bottom of the display to switch screens. 104 | * https://reactnavigation.org/docs/bottom-tab-navigator 105 | */ 106 | const BottomTab = createBottomTabNavigator(); 107 | 108 | function BottomTabNavigator() { 109 | const colorScheme = useColorScheme(); 110 | 111 | return ( 112 | 118 | ) => ({ 122 | title: "Tab One", 123 | tabBarIcon: ({ color }) => , 124 | })} 125 | /> 126 | ( 132 | 133 | ), 134 | }} 135 | /> 136 | , 142 | }} 143 | /> 144 | 145 | ); 146 | } 147 | 148 | /** 149 | * You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ 150 | */ 151 | function TabBarIcon(props: { 152 | name: React.ComponentProps["name"]; 153 | color: string; 154 | }) { 155 | return ; 156 | } 157 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtialevents", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "test": "jest --watchAll" 11 | }, 12 | "jest": { 13 | "preset": "jest-expo" 14 | }, 15 | "dependencies": { 16 | "@apollo/client": "^3.7.1", 17 | "@expo/vector-icons": "^13.0.0", 18 | "@nhost/react": "^0.15.0", 19 | "@nhost/react-apollo": "^4.9.0", 20 | "@react-navigation/bottom-tabs": "^6.0.5", 21 | "@react-navigation/native": "^6.0.2", 22 | "@react-navigation/native-stack": "^6.1.0", 23 | "expo": "~47.0.8", 24 | "expo-asset": "~8.6.2", 25 | "expo-constants": "~14.0.2", 26 | "expo-font": "~11.0.1", 27 | "expo-linking": "~3.2.3", 28 | "expo-secure-store": "~12.0.0", 29 | "expo-splash-screen": "~0.17.5", 30 | "expo-status-bar": "~1.4.2", 31 | "expo-system-ui": "~2.0.1", 32 | "expo-web-browser": "~12.0.0", 33 | "graphql": "^15.8.0", 34 | "react": "18.1.0", 35 | "react-dom": "18.1.0", 36 | "react-hook-form": "^7.39.5", 37 | "react-native": "0.70.5", 38 | "react-native-calendars": "^1.1291.1", 39 | "react-native-safe-area-context": "4.4.1", 40 | "react-native-screens": "~3.18.0", 41 | "react-native-web": "~0.18.9", 42 | "stream-chat-expo": "^5.5.1", 43 | "@react-native-community/netinfo": "9.3.5", 44 | "expo-av": "~13.0.1", 45 | "expo-document-picker": "~11.0.1", 46 | "expo-file-system": "~15.1.1", 47 | "expo-haptics": "~12.0.1", 48 | "expo-image-manipulator": "~11.0.0", 49 | "expo-image-picker": "~14.0.2", 50 | "expo-media-library": "~15.0.0", 51 | "expo-sharing": "~11.0.1", 52 | "react-native-gesture-handler": "~2.8.0", 53 | "react-native-reanimated": "~2.12.0", 54 | "react-native-svg": "13.4.0", 55 | "expo-clipboard": "~4.0.1" 56 | }, 57 | "devDependencies": { 58 | "@babel/core": "^7.12.9", 59 | "@types/react": "~18.0.24", 60 | "@types/react-native": "~0.70.6", 61 | "jest": "^26.6.3", 62 | "jest-expo": "~47.0.1", 63 | "react-test-renderer": "18.1.0", 64 | "typescript": "^4.6.3" 65 | }, 66 | "private": true 67 | } 68 | -------------------------------------------------------------------------------- /screens/AuthScreens/SignInScreen/SignInScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | View, 4 | Image, 5 | StyleSheet, 6 | useWindowDimensions, 7 | ScrollView, 8 | Alert, 9 | } from "react-native"; 10 | import Logo from "./logo.png"; 11 | import CustomInput from "../components/CustomInput"; 12 | import CustomButton from "../components/CustomButton"; 13 | import SocialSignInButtons from "../components/SocialSignInButtons"; 14 | import { useNavigation } from "@react-navigation/native"; 15 | import { useForm } from "react-hook-form"; 16 | import { useSignInEmailPassword } from "@nhost/react"; 17 | 18 | const EMAIL_REGEX = 19 | /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; 20 | 21 | const SignInScreen = () => { 22 | const navigation = useNavigation(); 23 | 24 | const { control, handleSubmit } = useForm(); 25 | 26 | const { signInEmailPassword, isLoading } = useSignInEmailPassword(); 27 | 28 | const onSignInPressed = async (data) => { 29 | if (isLoading) { 30 | return; 31 | } 32 | const { email, password } = data; 33 | const { error, needsEmailVerification } = await signInEmailPassword( 34 | email, 35 | password 36 | ); 37 | 38 | if (error) { 39 | Alert.alert("Oops", error.message); 40 | } 41 | 42 | if (needsEmailVerification) { 43 | Alert.alert("Verify you email", "Check your email and follow the link"); 44 | } 45 | }; 46 | 47 | const onForgotPasswordPressed = () => { 48 | // navigation.navigate("ForgotPassword"); 49 | }; 50 | 51 | const onSignUpPress = () => { 52 | navigation.navigate("SignUp"); 53 | }; 54 | 55 | return ( 56 | 57 | 58 | 59 | 68 | 69 | 82 | 83 | 87 | 88 | 93 | 94 | 95 | 96 | 101 | 102 | 103 | ); 104 | }; 105 | 106 | const styles = StyleSheet.create({ 107 | root: { 108 | backgroundColor: "white", 109 | minHeight: "100%", 110 | }, 111 | container: { 112 | padding: 20, 113 | }, 114 | logo: { 115 | width: "100%", 116 | height: undefined, 117 | aspectRatio: 16 / 9, 118 | }, 119 | }); 120 | 121 | export default SignInScreen; 122 | -------------------------------------------------------------------------------- /screens/AuthScreens/SignInScreen/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './SignInScreen'; -------------------------------------------------------------------------------- /screens/AuthScreens/SignInScreen/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/VirtualEvents/56f820028a88ed1a7ef8455fa07eb0b992bbff1c/screens/AuthScreens/SignInScreen/logo.png -------------------------------------------------------------------------------- /screens/AuthScreens/SignUpScreen/SignUpScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Text, StyleSheet, ScrollView, Alert } from "react-native"; 3 | import CustomInput from "../components/CustomInput"; 4 | import CustomButton from "../components/CustomButton"; 5 | import SocialSignInButtons from "../components/SocialSignInButtons"; 6 | import { useNavigation } from "@react-navigation/core"; 7 | import { useForm } from "react-hook-form"; 8 | import { useSignUpEmailPassword } from "@nhost/react"; 9 | 10 | const EMAIL_REGEX = 11 | /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; 12 | 13 | const SignUpScreen = () => { 14 | const { control, handleSubmit, watch } = useForm(); 15 | const pwd = watch("password"); 16 | const navigation = useNavigation(); 17 | 18 | const { signUpEmailPassword, isLoading } = useSignUpEmailPassword(); 19 | 20 | const onRegisterPressed = async (data) => { 21 | if (isLoading) { 22 | return; 23 | } 24 | const { name, email, password } = data; 25 | // sign up 26 | const { error, isSuccess, needsEmailVerification } = 27 | await signUpEmailPassword(email, password, { 28 | displayName: name.trim(), 29 | metadata: { name }, 30 | }); 31 | 32 | if (error) { 33 | Alert.alert("Oops", error.message); 34 | } 35 | 36 | if (needsEmailVerification) { 37 | Alert.alert("Verify you email", "Check your email and follow the link"); 38 | } 39 | 40 | if (isSuccess) { 41 | navigation.navigate("SignIn"); 42 | } 43 | }; 44 | 45 | const onSignInPress = () => { 46 | navigation.navigate("SignIn"); 47 | }; 48 | 49 | const onTermsOfUsePressed = () => { 50 | console.warn("onTermsOfUsePressed"); 51 | }; 52 | 53 | const onPrivacyPressed = () => { 54 | console.warn("onPrivacyPressed"); 55 | }; 56 | 57 | return ( 58 | 59 | 60 | Create an account 61 | 62 | 78 | 79 | 88 | 101 | value === pwd || "Password do not match", 108 | }} 109 | /> 110 | 111 | 115 | 116 | 117 | By registering, you confirm that you accept our{" "} 118 | 119 | Terms of Use 120 | {" "} 121 | and{" "} 122 | 123 | Privacy Policy 124 | 125 | 126 | 127 | 128 | 129 | 134 | 135 | 136 | ); 137 | }; 138 | 139 | const styles = StyleSheet.create({ 140 | root: { 141 | alignItems: "center", 142 | padding: 20, 143 | }, 144 | title: { 145 | fontSize: 24, 146 | fontWeight: "bold", 147 | color: "#051C60", 148 | margin: 10, 149 | }, 150 | text: { 151 | color: "gray", 152 | marginVertical: 10, 153 | }, 154 | link: { 155 | color: "#FDB075", 156 | }, 157 | }); 158 | 159 | export default SignUpScreen; 160 | -------------------------------------------------------------------------------- /screens/AuthScreens/SignUpScreen/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './SignUpScreen'; 2 | -------------------------------------------------------------------------------- /screens/AuthScreens/components/CustomButton/CustomButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, StyleSheet, Pressable } from "react-native"; 3 | 4 | type CustomButtonProps = { 5 | onPress: () => void; 6 | text: string; 7 | type?: "PRIMARY" | "SECONDARY" | "TERTIARY"; 8 | bgColor?: string; 9 | fgColor?: string; 10 | }; 11 | 12 | const CustomButton = ({ 13 | onPress, 14 | text, 15 | type = "PRIMARY", 16 | bgColor, 17 | fgColor, 18 | }: CustomButtonProps) => { 19 | return ( 20 | 28 | 35 | {text} 36 | 37 | 38 | ); 39 | }; 40 | 41 | const styles = StyleSheet.create({ 42 | container: { 43 | width: "100%", 44 | 45 | padding: 15, 46 | marginVertical: 5, 47 | 48 | alignItems: "center", 49 | borderRadius: 5, 50 | }, 51 | 52 | container_PRIMARY: { 53 | backgroundColor: "#3B71F3", 54 | }, 55 | 56 | container_SECONDARY: { 57 | borderColor: "#3B71F3", 58 | borderWidth: 2, 59 | }, 60 | 61 | container_TERTIARY: {}, 62 | 63 | text: { 64 | fontWeight: "bold", 65 | color: "white", 66 | }, 67 | 68 | text_PRIMARY: {}, 69 | 70 | text_SECONDARY: { 71 | color: "#3B71F3", 72 | }, 73 | 74 | text_TERTIARY: { 75 | color: "gray", 76 | }, 77 | }); 78 | 79 | export default CustomButton; 80 | -------------------------------------------------------------------------------- /screens/AuthScreens/components/CustomButton/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './CustomButton'; 2 | -------------------------------------------------------------------------------- /screens/AuthScreens/components/CustomInput/CustomInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | View, 4 | Text, 5 | TextInput, 6 | StyleSheet, 7 | TextInputProps, 8 | } from "react-native"; 9 | import { Control, Controller } from "react-hook-form"; 10 | 11 | type CustomInputProps = TextInputProps & { 12 | control: Control; 13 | name: string; 14 | rules: {}; 15 | }; 16 | 17 | const CustomInput = ({ 18 | control, 19 | name, 20 | rules = {}, 21 | ...inputProps 22 | }: CustomInputProps) => { 23 | return ( 24 | ( 32 | <> 33 | 39 | 46 | 47 | {error && ( 48 | 49 | {error.message || "Error"} 50 | 51 | )} 52 | 53 | )} 54 | /> 55 | ); 56 | }; 57 | 58 | const styles = StyleSheet.create({ 59 | container: { 60 | backgroundColor: "white", 61 | width: "100%", 62 | 63 | borderColor: "#e8e8e8", 64 | borderWidth: 1, 65 | borderRadius: 5, 66 | 67 | padding: 15, 68 | marginVertical: 5, 69 | }, 70 | input: {}, 71 | }); 72 | 73 | export default CustomInput; 74 | -------------------------------------------------------------------------------- /screens/AuthScreens/components/CustomInput/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './CustomInput'; 2 | -------------------------------------------------------------------------------- /screens/AuthScreens/components/SocialSignInButtons/SocialSignInButtons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Text} from 'react-native'; 3 | import CustomButton from '../CustomButton'; 4 | 5 | const SocialSignInButtons = () => { 6 | const onSignInFacebook = () => { 7 | console.warn('onSignInFacebook'); 8 | }; 9 | 10 | const onSignInGoogle = () => { 11 | console.warn('onSignInGoogle'); 12 | }; 13 | 14 | const onSignInApple = () => { 15 | console.warn('onSignInApple'); 16 | }; 17 | 18 | return ( 19 | <> 20 | 26 | 32 | 38 | 39 | ); 40 | }; 41 | 42 | export default SocialSignInButtons; 43 | -------------------------------------------------------------------------------- /screens/AuthScreens/components/SocialSignInButtons/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './SocialSignInButtons'; 2 | -------------------------------------------------------------------------------- /screens/Chat/ChatRoomScreen.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from "react-native"; 2 | import React, { useEffect } from "react"; 3 | import { useChatContext } from "../../context/ChatContext"; 4 | import { Channel, MessageList, MessageInput } from "stream-chat-expo"; 5 | import { useNavigation } from "@react-navigation/native"; 6 | 7 | const ChatRoomScreen = () => { 8 | const { currentChannel } = useChatContext(); 9 | const navigation = useNavigation(); 10 | 11 | useEffect(() => { 12 | navigation.setOptions({ title: currentChannel?.data?.name || "Channel" }); 13 | }, [currentChannel?.data?.name]); 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default ChatRoomScreen; 24 | -------------------------------------------------------------------------------- /screens/Chat/ChatsScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useChatContext } from "../../context/ChatContext"; 3 | import { ChannelList } from "stream-chat-expo"; 4 | import { Channel } from "stream-chat"; 5 | import { useNavigation } from "@react-navigation/native"; 6 | 7 | const ChatsScreen = () => { 8 | const { setCurrentChannel } = useChatContext(); 9 | 10 | const navigation = useNavigation(); 11 | 12 | const onSelect = (chanel: Channel) => { 13 | setCurrentChannel(chanel); 14 | navigation.navigate("ChatRoom"); 15 | }; 16 | 17 | return ; 18 | }; 19 | 20 | export default ChatsScreen; 21 | -------------------------------------------------------------------------------- /screens/ModalScreen.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from "expo-status-bar"; 2 | import { 3 | Platform, 4 | StyleSheet, 5 | Image, 6 | ActivityIndicator, 7 | Alert, 8 | } from "react-native"; 9 | 10 | import { View, Text } from "../components/Themed"; 11 | import { AntDesign } from "@expo/vector-icons"; 12 | import CustomButton from "../components/CustomButton"; 13 | import { gql, useQuery, useMutation } from "@apollo/client"; 14 | import { useUserId } from "@nhost/react"; 15 | import { useChatContext } from "../context/ChatContext"; 16 | 17 | const GetEvent = gql` 18 | query GetEvent($id: uuid!) { 19 | Event_by_pk(id: $id) { 20 | id 21 | name 22 | date 23 | EventAttendee { 24 | user { 25 | id 26 | displayName 27 | avatarUrl 28 | } 29 | } 30 | } 31 | } 32 | `; 33 | 34 | const JoinEvent = gql` 35 | mutation InsertEventAttendee($eventId: uuid!, $userId: uuid!) { 36 | insert_EventAttendee(objects: [{ eventId: $eventId, userId: $userId }]) { 37 | returning { 38 | id 39 | userId 40 | eventId 41 | Event { 42 | id 43 | EventAttendee { 44 | id 45 | } 46 | } 47 | } 48 | } 49 | } 50 | `; 51 | 52 | export default function ModalScreen({ route }) { 53 | const id = route?.params?.id; 54 | const userId = useUserId(); 55 | 56 | const { data, loading, error } = useQuery(GetEvent, { variables: { id } }); 57 | const event = data?.Event_by_pk; 58 | 59 | const [doJoinEvent] = useMutation(JoinEvent); 60 | 61 | const { joinEventChatRoom } = useChatContext(); 62 | 63 | const onJoin = async () => { 64 | try { 65 | await doJoinEvent({ variables: { userId, eventId: id } }); 66 | } catch (e) { 67 | Alert.alert("Failed to join the event", error?.message); 68 | } 69 | }; 70 | 71 | const displayedUsers = (event?.EventAttendee || []) 72 | .slice(0, 5) 73 | .map((attendee) => attendee.user); 74 | 75 | const joined = event?.EventAttendee?.some( 76 | (attendee) => attendee.user.id === userId 77 | ); 78 | 79 | if (error) { 80 | return ( 81 | 82 | Couldn't find the event 83 | {error.message} 84 | 85 | ); 86 | } 87 | 88 | if (loading) { 89 | return ; 90 | } 91 | 92 | return ( 93 | 94 | {event.name} 95 | 96 | 97 | {" "} 98 | {new Date(event.date).toDateString()} 99 | 100 | 101 | 102 | {/* User avatars */} 103 | 104 | {displayedUsers.map((user, index) => ( 105 | 113 | ))} 114 | 120 | +{event?.EventAttendee?.length - displayedUsers.length} 121 | 122 | 123 | 124 | {!joined ? ( 125 | 126 | ) : ( 127 | joinEventChatRoom(event)} 131 | /> 132 | )} 133 | 134 | 135 | {/* Use a light status bar on iOS to account for the black space above the modal */} 136 | 137 | 138 | ); 139 | } 140 | 141 | const styles = StyleSheet.create({ 142 | container: { 143 | flex: 1, 144 | padding: 10, 145 | paddingBottom: 25, 146 | }, 147 | title: { 148 | fontSize: 24, 149 | fontWeight: "bold", 150 | marginVertical: 10, 151 | }, 152 | time: { 153 | fontSize: 20, 154 | }, 155 | footer: { 156 | marginTop: "auto", 157 | }, 158 | users: { 159 | flexDirection: "row", 160 | }, 161 | userAvatar: { 162 | width: 50, 163 | aspectRatio: 1, 164 | borderRadius: 25, 165 | margin: 2, 166 | borderWidth: 2, 167 | borderColor: "white", 168 | backgroundColor: "gainsboro", 169 | justifyContent: "center", 170 | alignItems: "center", 171 | }, 172 | }); 173 | -------------------------------------------------------------------------------- /screens/NotFoundScreen.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, TouchableOpacity } from 'react-native'; 2 | 3 | import { Text, View } from '../components/Themed'; 4 | import { RootStackScreenProps } from '../types'; 5 | 6 | export default function NotFoundScreen({ navigation }: RootStackScreenProps<'NotFound'>) { 7 | return ( 8 | 9 | This screen doesn't exist. 10 | navigation.replace('Root')} style={styles.link}> 11 | Go to home screen! 12 | 13 | 14 | ); 15 | } 16 | 17 | const styles = StyleSheet.create({ 18 | container: { 19 | flex: 1, 20 | alignItems: 'center', 21 | justifyContent: 'center', 22 | padding: 20, 23 | }, 24 | title: { 25 | fontSize: 20, 26 | fontWeight: 'bold', 27 | }, 28 | link: { 29 | marginTop: 15, 30 | paddingVertical: 15, 31 | }, 32 | linkText: { 33 | fontSize: 14, 34 | color: '#2e78b7', 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /screens/TabOneScreen.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | StyleSheet, 3 | Text, 4 | Pressable, 5 | Alert, 6 | ActivityIndicator, 7 | } from "react-native"; 8 | import { View } from "../components/Themed"; 9 | import { RootTabScreenProps } from "../types"; 10 | import { Agenda, AgendaEntry, AgendaSchedule } from "react-native-calendars"; 11 | import { gql, useQuery } from "@apollo/client"; 12 | 13 | const GetEvents = gql` 14 | query GetEvents { 15 | Event { 16 | id 17 | name 18 | date 19 | } 20 | } 21 | `; 22 | 23 | const getEventsSchedule = (events: []): AgendaSchedule => { 24 | const items: AgendaSchedule = {}; 25 | 26 | events.forEach((event) => { 27 | const day = event.date.slice(0, 10); 28 | 29 | if (!items[day]) { 30 | items[day] = []; 31 | } 32 | items[day].push({ ...event, day, height: 50 }); 33 | }); 34 | 35 | return items; 36 | }; 37 | 38 | export default function TabOneScreen({ 39 | navigation, 40 | }: RootTabScreenProps<"TabOne">) { 41 | const { data, loading, error } = useQuery(GetEvents); 42 | 43 | const renderItem = (reservation: AgendaEntry, isFirst: boolean) => { 44 | const fontSize = isFirst ? 16 : 14; 45 | const color = isFirst ? "black" : "#43515c"; 46 | 47 | return ( 48 | navigation.navigate("Modal", { id: reservation.id })} 51 | > 52 | {reservation.name} 53 | 54 | ); 55 | }; 56 | 57 | const renderEmptyDate = () => { 58 | return ( 59 | 60 | This is empty date! 61 | 62 | ); 63 | }; 64 | 65 | if (loading) { 66 | return ; 67 | } 68 | 69 | if (error) { 70 | Alert.alert("Error fetching events", error.message); 71 | } 72 | 73 | const events = getEventsSchedule(data.Event); 74 | 75 | return ( 76 | 77 | 83 | 84 | ); 85 | } 86 | 87 | const styles = StyleSheet.create({ 88 | container: { 89 | flex: 1, 90 | }, 91 | item: { 92 | backgroundColor: "white", 93 | flex: 1, 94 | borderRadius: 5, 95 | padding: 10, 96 | marginRight: 10, 97 | marginTop: 17, 98 | }, 99 | emptyDate: { 100 | height: 15, 101 | flex: 1, 102 | paddingTop: 30, 103 | }, 104 | }); 105 | -------------------------------------------------------------------------------- /screens/TabTwoScreen.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Image } from "react-native"; 2 | 3 | import { Text, View } from "../components/Themed"; 4 | import CustomButton from "../components/CustomButton"; 5 | import { useUserData, useSignOut } from "@nhost/react"; 6 | 7 | export default function TabTwoScreen() { 8 | const user = useUserData(); 9 | const { signOut } = useSignOut(); 10 | 11 | return ( 12 | 13 | 14 | {user?.displayName} 15 | 16 | 22 | 23 | 24 | ); 25 | } 26 | 27 | const styles = StyleSheet.create({ 28 | container: { 29 | flex: 1, 30 | padding: 10, 31 | alignItems: "center", 32 | }, 33 | avatar: { 34 | width: 100, 35 | aspectRatio: 1, 36 | borderRadius: 50, 37 | }, 38 | name: { 39 | fontWeight: "bold", 40 | fontSize: 22, 41 | marginVertical: 15, 42 | color: "dimgray", 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /screens/UsersScreen.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, FlatList, ActivityIndicator } from "react-native"; 2 | import UserListItem from "../components/UserListItem"; 3 | import { gql, useQuery } from "@apollo/client"; 4 | 5 | const GetUser = gql` 6 | query GetUsers { 7 | users { 8 | id 9 | displayName 10 | avatarUrl 11 | } 12 | } 13 | `; 14 | 15 | const UsersScreen = () => { 16 | const { data, loading, error } = useQuery(GetUser); 17 | 18 | if (loading) { 19 | return ; 20 | } 21 | 22 | if (error) { 23 | return {error.message}; 24 | } 25 | 26 | console.log(JSON.stringify(data, null, 2)); 27 | 28 | return ( 29 | } 32 | /> 33 | ); 34 | }; 35 | 36 | export default UsersScreen; 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /types.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about using TypeScript with React Navigation: 3 | * https://reactnavigation.org/docs/typescript/ 4 | */ 5 | 6 | import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; 7 | import { 8 | CompositeScreenProps, 9 | NavigatorScreenParams, 10 | } from "@react-navigation/native"; 11 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 12 | 13 | declare global { 14 | namespace ReactNavigation { 15 | interface RootParamList extends RootStackParamList {} 16 | } 17 | } 18 | 19 | export type RootStackParamList = { 20 | Root: NavigatorScreenParams | undefined; 21 | Modal: { id: string }; 22 | Users: undefined; 23 | NotFound: undefined; 24 | SignIn: undefined; 25 | SignUp: undefined; 26 | }; 27 | 28 | export type RootStackScreenProps = 29 | NativeStackScreenProps; 30 | 31 | export type RootTabParamList = { 32 | TabOne: undefined; 33 | TabTwo: undefined; 34 | }; 35 | 36 | export type RootTabScreenProps = 37 | CompositeScreenProps< 38 | BottomTabScreenProps, 39 | NativeStackScreenProps 40 | >; 41 | --------------------------------------------------------------------------------