├── .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 |
--------------------------------------------------------------------------------