diff --git a/packages/app/.eslintrc.json b/packages/app/.eslintrc.json index c12c167c..888ce976 100644 --- a/packages/app/.eslintrc.json +++ b/packages/app/.eslintrc.json @@ -1,17 +1,23 @@ { - "plugins": ["react", "react-native"], + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["react", "react-native","@typescript-eslint"], "env": { "react-native/react-native": true }, "parserOptions": { "sourceType": "module", "ecmaVersion": 2023, - "ecmaFeatures": { "jsx": true } + "ecmaFeatures": { "jsx": true }, + "project": ["./tsconfig.json"], + "tsconfigRootDir": "packages/app/" }, + "ignorePatterns": ["babel.config.js", "metro.config.js"], "extends": [ "prettier", "eslint:recommended", "plugin:react-native/all", - "plugin:react/recommended" + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended" ] } diff --git a/packages/app/Root.js b/packages/app/Root.tsx similarity index 88% rename from packages/app/Root.js rename to packages/app/Root.tsx index 6c264c87..a6b1a68a 100644 --- a/packages/app/Root.js +++ b/packages/app/Root.tsx @@ -21,9 +21,7 @@ import Constants from "expo-constants"; const Tab = createBottomTabNavigator(); const Stack = createNativeStackNavigator(); -import { - GoogleSignin, -} from "@react-native-google-signin/google-signin"; +import { GoogleSignin } from "@react-native-google-signin/google-signin"; function TabNavigation() { return ( @@ -39,7 +37,8 @@ function App() { const { user, setUser } = useUserContext(); const currentSession = async () => { - let accessToken = await SecureStore.getValueFor("accessToken"); + const accessToken = await SecureStore.getValueFor("accessToken"); + let refreshToken = await SecureStore.getValueFor("refreshToken"); if (!accessToken) { return; @@ -68,7 +67,7 @@ function App() { } // If access token is invalid/expired, try to get a new one with the refresh token - const refreshToken = await SecureStore.getValueFor("refreshToken"); + refreshToken = await SecureStore.getValueFor("refreshToken"); try { const { data: response } = await axios.post(`${ENDPOINT}/auth/token`, { refreshToken: refreshToken, @@ -97,10 +96,10 @@ function App() { useEffect(() => { GoogleSignin.configure({ - webClientId: Constants.expoConfig.extra.webClientId, - iosClientId: Constants.expoConfig.extra.iosClientId, + webClientId: Constants?.expoConfig?.extra?.webClientId, + iosClientId: Constants?.expoConfig?.extra?.iosClientId, }); - + currentSession(); }, []); diff --git a/packages/app/__tests__/FeedDrawer.test.js b/packages/app/__tests__/FeedDrawer.test.tsx similarity index 97% rename from packages/app/__tests__/FeedDrawer.test.js rename to packages/app/__tests__/FeedDrawer.test.tsx index 104dd936..08bd3f50 100644 --- a/packages/app/__tests__/FeedDrawer.test.js +++ b/packages/app/__tests__/FeedDrawer.test.tsx @@ -2,7 +2,6 @@ import React from "react"; import FeedDrawer from "../screens/feed/FeedDrawer"; import { render } from "@testing-library/react-native"; import { NavigationContainer } from "@react-navigation/native"; -import jest, { test, expect } from "jest"; // This section is used to mock the user data information within the FeedScreen. // Without this, Jest does not recognize the user data and throws an error. diff --git a/packages/app/__tests__/GroupScreen.test.js b/packages/app/__tests__/GroupScreen.test.tsx similarity index 82% rename from packages/app/__tests__/GroupScreen.test.js rename to packages/app/__tests__/GroupScreen.test.tsx index 07026779..382ee31e 100644 --- a/packages/app/__tests__/GroupScreen.test.js +++ b/packages/app/__tests__/GroupScreen.test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { render } from "@testing-library/react-native"; import GroupScreen from "@app/screens/group/GroupScreen"; import GroupHeader from "@app/components/GroupScreen/GroupHeader"; -import jest, { describe, it, expect } from "jest"; +import { GroupHeaderProps } from "@app/types/Group"; jest.mock("@expo/vector-icons/Ionicons", () => ({ Ionicons: () => null, @@ -12,6 +12,8 @@ jest.mock("@expo/vector-icons/FontAwesome5", () => ({ FontAwesome5: () => null, })); +let props: GroupHeaderProps; + describe("GroupScreen Tests", () => { describe(GroupScreen, () => { it("renders the GroupHeader component", () => { @@ -35,25 +37,25 @@ describe("GroupScreen Tests", () => { describe(GroupHeader, () => { it("renders group icon", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const groupIcon = getByTestId("groupIcon"); expect(groupIcon).toBeTruthy(); }); it("renders club banner", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const clubBanner = getByTestId("clubBanner"); expect(clubBanner).toBeTruthy(); }); it("renders group header info", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const groupHeaderInfo = getByTestId("groupHeaderInfo"); expect(groupHeaderInfo).toBeTruthy(); }); it("renders group media icons", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const groupMediaIcon = getByTestId("groupMediaIcon"); expect(groupMediaIcon).toBeTruthy(); }); diff --git a/packages/app/__tests__/Login.test.js b/packages/app/__tests__/Login.test.tsx similarity index 79% rename from packages/app/__tests__/Login.test.js rename to packages/app/__tests__/Login.test.tsx index 965b0870..9918d438 100644 --- a/packages/app/__tests__/Login.test.js +++ b/packages/app/__tests__/Login.test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render, fireEvent } from "@testing-library/react-native"; import LandingScreen from "@app/screens/landing/LandingScreen"; -import jest, { describe, it, expect } from "jest"; +import { LandingScreenNavigationProps } from "@app/types/Landing"; // Need these to avoid "Cannot use import statement outside a module" error jest.mock("expo-web-browser", () => ({ @@ -14,16 +14,23 @@ jest.mock("expo-constants", () => ({ Constants: () => null, })); +let props: LandingScreenNavigationProps; +const mockedNavigate = jest.fn(); + +jest.mock("@react-navigation/native", () => ({ + useNavigation: () => ({ navigate: mockedNavigate }), +})); + describe(LandingScreen, () => { it("renders logo text", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const logo = getByTestId("logo"); expect(logo).toBeTruthy(); }); describe("Email/Password Text Inputs", () => { it("renders correctly", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const emailInput = getByTestId("emailInput.textInput"); const passwordInput = getByTestId("passwordInput.textInput"); expect(emailInput).toBeTruthy(); @@ -31,7 +38,7 @@ describe(LandingScreen, () => { }); it("displays the placeholder text", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const emailInput = getByTestId("emailInput.textInput"); const passwordInput = getByTestId("passwordInput.textInput"); @@ -41,7 +48,7 @@ describe(LandingScreen, () => { }); it("accepts inputs", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const emailInput = getByTestId("emailInput.textInput"); const passwordInput = getByTestId("passwordInput.textInput"); @@ -54,7 +61,9 @@ describe(LandingScreen, () => { }); it("doesn't accept an invalid email/password", () => { - const { queryByTestId, getByTestId } = render(); + const { queryByTestId, getByTestId } = render( + + ); const loginButton = getByTestId("loginButton"); const emailInput = getByTestId("emailInput.textInput"); const passwordInput = getByTestId("passwordInput.textInput"); @@ -69,7 +78,7 @@ describe(LandingScreen, () => { }); it("visibility icon toggles password visibility", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const hidePasswordButton = getByTestId("passwordInput.visibility"); const passwordInput = getByTestId("passwordInput.textInput"); @@ -81,25 +90,22 @@ describe(LandingScreen, () => { describe("Buttons", () => { it("login button renders correctly", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const loginButton = getByTestId("loginButton"); expect(loginButton).toBeTruthy(); fireEvent.press(loginButton); }); it("google button renders correctly", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const googleButton = getByTestId("googleButton"); expect(googleButton).toBeTruthy(); fireEvent.press(googleButton); }); it("signup button renders correctly", () => { - const mockNavigation = { - navigate: jest.fn(), - }; const { getByTestId } = render( - + ); const signupButton = getByTestId("signupButton"); expect(signupButton).toBeTruthy(); @@ -107,7 +113,7 @@ describe(LandingScreen, () => { }); it("forgot password button renders correctly", () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const forgotPassButton = getByTestId("forgotPassButton"); expect(forgotPassButton).toBeTruthy(); fireEvent.press(forgotPassButton); diff --git a/packages/app/__tests__/todo.test.js b/packages/app/__tests__/todo.test.tsx similarity index 57% rename from packages/app/__tests__/todo.test.js rename to packages/app/__tests__/todo.test.tsx index 4cb2b25a..028592b3 100644 --- a/packages/app/__tests__/todo.test.js +++ b/packages/app/__tests__/todo.test.tsx @@ -1,5 +1,3 @@ -import { test, expect } from "jest"; - test("test", () => { expect(true).toBe(true); }); diff --git a/packages/app/app.config.js b/packages/app/app.config.ts similarity index 96% rename from packages/app/app.config.js rename to packages/app/app.config.ts index 3524a07e..668ab984 100644 --- a/packages/app/app.config.js +++ b/packages/app/app.config.ts @@ -1,4 +1,5 @@ -require("dotenv").config(); +import dotenv from 'dotenv'; +dotenv.config(); module.exports = { name: "Icebreak", diff --git a/packages/app/assets/eye-line-off.js b/packages/app/assets/eye-line-off.tsx similarity index 96% rename from packages/app/assets/eye-line-off.js rename to packages/app/assets/eye-line-off.tsx index e25fa10a..cb57e767 100644 --- a/packages/app/assets/eye-line-off.js +++ b/packages/app/assets/eye-line-off.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import Svg, { Path } from "react-native-svg"; -const EyeOff = (props) => ( +const EyeOff = (props: any) => ( ( +const EyeOn = (props: any) => ( ( +const GoogleIcon = (props: any) => ( + {props.icon} @@ -35,12 +39,4 @@ function Button(props) { ); } -Button.propTypes = { - fontColor: PropTypes.string, - fontWeight: PropTypes.string, - icon: PropTypes.node, - textStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - title: PropTypes.string, -}; - export default Button; diff --git a/packages/app/components/DividerWithText.js b/packages/app/components/DividerWithText.tsx similarity index 85% rename from packages/app/components/DividerWithText.js rename to packages/app/components/DividerWithText.tsx index 12d4d36a..c96ee53e 100644 --- a/packages/app/components/DividerWithText.js +++ b/packages/app/components/DividerWithText.tsx @@ -1,6 +1,6 @@ import React from "react"; import { StyleSheet, View, Text } from "react-native"; -import PropTypes from "prop-types"; +import { DividerWithTextProps } from "@app/types/DividerWithText"; const GRAY = "#c4c4c4"; @@ -27,7 +27,7 @@ const styles = StyleSheet.create({ }, }); -function DividerWithText(props) { +function DividerWithText(props: DividerWithTextProps) { return ( @@ -37,8 +37,4 @@ function DividerWithText(props) { ); } -DividerWithText.propTypes = { - title: PropTypes.string, -}; - export default DividerWithText; diff --git a/packages/app/components/EventCard/EventCard.js b/packages/app/components/EventCard/EventCard.tsx similarity index 76% rename from packages/app/components/EventCard/EventCard.js rename to packages/app/components/EventCard/EventCard.tsx index 18bfefb0..7e816399 100644 --- a/packages/app/components/EventCard/EventCard.js +++ b/packages/app/components/EventCard/EventCard.tsx @@ -1,11 +1,12 @@ -import React from "react"; +import React, { FC } from "react"; import { Image, StyleSheet, View } from "react-native"; import EventCardText from "./EventCardText"; import EventCardRegistration from "./EventCardRegistration"; -import PropTypes from "prop-types"; +import { EventCardProps } from "@app/types/EventCard"; const cardColor = "white"; +// Stylesheet for the EventCard component const styles = StyleSheet.create({ banner: { borderTopLeftRadius: 15, @@ -26,7 +27,7 @@ const styles = StyleSheet.create({ }, }); -const EventCard = ({ +const EventCard: FC = ({ style, banner, title, @@ -56,14 +57,4 @@ const EventCard = ({ ); }; -EventCard.propTypes = { - banner: PropTypes.number, - description: PropTypes.string, - location: PropTypes.string, - style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - timeBegin: PropTypes.string, - timeEnd: PropTypes.string, - title: PropTypes.string, -}; - export default EventCard; diff --git a/packages/app/components/EventCard/EventCardRegistration.js b/packages/app/components/EventCard/EventCardRegistration.tsx similarity index 67% rename from packages/app/components/EventCard/EventCardRegistration.js rename to packages/app/components/EventCard/EventCardRegistration.tsx index d7f770fd..97fed7a7 100644 --- a/packages/app/components/EventCard/EventCardRegistration.js +++ b/packages/app/components/EventCard/EventCardRegistration.tsx @@ -1,13 +1,19 @@ import React from "react"; -import { StyleSheet, Button, View } from "react-native"; +import { StyleSheet, View } from "react-native"; +import Button from "../Button"; import FaceIcon from "./FaceIcon"; -import PropTypes from "prop-types"; +import { EventCardRegistrationProps } from "@app/types/EventCard"; // Sample array for testing const sampleArray = [0, 1, 2, 3]; +const BLUE = "#0b91e0"; + const styles = StyleSheet.create({ button: { + backgroundColor: BLUE, + borderRadius: 5, + height: 35, marginBottom: 0, marginLeft: 0, marginRight: 0, @@ -26,13 +32,20 @@ const styles = StyleSheet.create({ }, }); -function EventCardRegistration(props) { +export default function EventCardRegistration( + props: EventCardRegistrationProps +) { const { register } = props; return ( - + {sampleArray.slice(0, 4).map((x) => { @@ -50,9 +63,3 @@ function EventCardRegistration(props) { ); } - -EventCardRegistration.propTypes = { - register: PropTypes.func, -}; - -export default EventCardRegistration; diff --git a/packages/app/components/EventCard/EventCardText.js b/packages/app/components/EventCard/EventCardText.tsx similarity index 69% rename from packages/app/components/EventCard/EventCardText.js rename to packages/app/components/EventCard/EventCardText.tsx index 13e02c5d..c3e7257d 100644 --- a/packages/app/components/EventCard/EventCardText.js +++ b/packages/app/components/EventCard/EventCardText.tsx @@ -1,6 +1,6 @@ +import { EventTextProps } from "@app/types/EventCard"; import React from "react"; import { StyleSheet, Text, View } from "react-native"; -import PropTypes from "prop-types"; const GRAY = "grey"; @@ -19,7 +19,13 @@ const styles = StyleSheet.create({ }, }); -function EventCardText({ timeBegin, timeEnd, title, location, description }) { +const EventCardText: React.FC = ({ + timeBegin, + timeEnd, + title, + location, + description, +}) => { return ( @@ -32,14 +38,6 @@ function EventCardText({ timeBegin, timeEnd, title, location, description }) { ); -} - -EventCardText.propTypes = { - description: PropTypes.string, - location: PropTypes.string, - timeBegin: PropTypes.string, - timeEnd: PropTypes.string, - title: PropTypes.string, }; export default EventCardText; diff --git a/packages/app/components/EventCard/FaceIcon.js b/packages/app/components/EventCard/FaceIcon.js deleted file mode 100644 index 18f1c3bf..00000000 --- a/packages/app/components/EventCard/FaceIcon.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; -import { StyleSheet, Image } from "react-native"; -import PropTypes from "prop-types"; - -function FaceIcon(props) { - const styles = StyleSheet.create({ - imageStyle: { - borderRadius: 18, - height: 36, - transform: [{ translateX: 30 - props.index * 20 }], - width: 36, - }, - }); - - return ( - - ); -} - -FaceIcon.propTypes = { - iconUrl: PropTypes.string, - index: PropTypes.number, -}; - -export default FaceIcon; diff --git a/packages/app/components/EventCard/FaceIcon.tsx b/packages/app/components/EventCard/FaceIcon.tsx new file mode 100644 index 00000000..dfb590e7 --- /dev/null +++ b/packages/app/components/EventCard/FaceIcon.tsx @@ -0,0 +1,22 @@ +import { FaceIconProps } from "@app/types/EventCard"; +import React from "react"; +import { Image, StyleSheet } from "react-native"; + +const FaceIcon: React.FC = (props) => { + const { index, iconUrl } = props; + + const styles = StyleSheet.create({ + imageStyle: { + borderRadius: 18, + height: 36, + transform: [{ translateX: 30 - props.index * 20 }], + width: 36, + }, + }); + + return ( + + ); +}; + +export default FaceIcon; diff --git a/packages/app/components/EventCard/eventcard_test/test_card_banner.png b/packages/app/components/EventCard/eventcard_test/test_card_banner.png new file mode 100644 index 00000000..a6844438 Binary files /dev/null and b/packages/app/components/EventCard/eventcard_test/test_card_banner.png differ diff --git a/packages/app/components/GroupScreen/GroupHeader.js b/packages/app/components/GroupScreen/GroupHeader.tsx similarity index 94% rename from packages/app/components/GroupScreen/GroupHeader.js rename to packages/app/components/GroupScreen/GroupHeader.tsx index dc1b9f00..7734d0fa 100644 --- a/packages/app/components/GroupScreen/GroupHeader.js +++ b/packages/app/components/GroupScreen/GroupHeader.tsx @@ -4,7 +4,7 @@ import { View, StyleSheet, Image } from "react-native"; import GroupIcon from "./GroupIcon"; import GroupHeaderInfo from "./GroupHeaderInfo"; import GroupMediaIcon from "./GroupMediaIcon"; -import PropTypes from "prop-types"; +import { GroupHeaderProps } from "@app/types/Group"; const bannerHeight = 110; const iconSize = 62; @@ -27,7 +27,7 @@ const testDiscordUrl = "https://discord.com"; const testLinkedInUrl = "https://linkedin.com"; const testInstagramUrl = "https://instagram.com"; -function GroupHeader(props) { +function GroupHeader(props: GroupHeaderProps) { const styles = StyleSheet.create({ bannerStyle: { height: bannerHeight, @@ -82,8 +82,4 @@ function GroupHeader(props) { ); } -GroupHeader.propTypes = { - testID: PropTypes.string, -}; - export default GroupHeader; diff --git a/packages/app/components/GroupScreen/GroupHeaderInfo.js b/packages/app/components/GroupScreen/GroupHeaderInfo.tsx similarity index 90% rename from packages/app/components/GroupScreen/GroupHeaderInfo.js rename to packages/app/components/GroupScreen/GroupHeaderInfo.tsx index 6915ce86..071ba14a 100644 --- a/packages/app/components/GroupScreen/GroupHeaderInfo.js +++ b/packages/app/components/GroupScreen/GroupHeaderInfo.tsx @@ -10,7 +10,7 @@ import Ionicons from "@expo/vector-icons/Ionicons"; import GroupTag from "./GroupTag"; -import PropTypes from "prop-types"; +import { GroupHeaderInfoProps } from "@app/types/Group"; const GRAY = "#2C2C2C"; const LIGHT_GRAY = "#6C6C6C"; @@ -82,7 +82,7 @@ const styles = StyleSheet.create({ * @param {string} props.url - Link to the website of org. * @param {string[]} props.tags - String array of tags related to org. */ -function GroupHeaderInfo(props) { +function GroupHeaderInfo(props: GroupHeaderInfoProps) { const [isDescriptionTruncated, setIsDescriptionTruncated] = useState(true); const toggleDescriptionTruncation = () => { @@ -144,16 +144,4 @@ function GroupHeaderInfo(props) { ); } -GroupHeaderInfo.propTypes = { - description: PropTypes.string, - handler: PropTypes.string, - location: PropTypes.string, - members: PropTypes.number, - name: PropTypes.string, - orgTags: PropTypes.arrayOf(PropTypes.string), - tags: PropTypes.arrayOf(PropTypes.string), - testID: PropTypes.string, - url: PropTypes.string, -}; - export default GroupHeaderInfo; diff --git a/packages/app/components/GroupScreen/GroupIcon.js b/packages/app/components/GroupScreen/GroupIcon.tsx similarity index 83% rename from packages/app/components/GroupScreen/GroupIcon.js rename to packages/app/components/GroupScreen/GroupIcon.tsx index 1a250237..33acc748 100644 --- a/packages/app/components/GroupScreen/GroupIcon.js +++ b/packages/app/components/GroupScreen/GroupIcon.tsx @@ -1,7 +1,7 @@ import React from "react"; import { View, StyleSheet, Image } from "react-native"; -import PropTypes from "prop-types"; +import { GroupIconProps } from "@app/types/Group"; const styles = StyleSheet.create({ iconContainer: { @@ -25,7 +25,7 @@ const styles = StyleSheet.create({ * @param {string} props.backgroundColor - Background color of icon. * @param {string} props.icon - Icon image source. */ -function GroupIcon(props) { +function GroupIcon(props: GroupIconProps) { return ( {props.githubUrl && ( @@ -91,13 +90,4 @@ function GroupMediaIcon(props) { ); } -GroupMediaIcon.propTypes = { - size: PropTypes.number, - testID: PropTypes.string, - githubUrl: PropTypes.string, - discordUrl: PropTypes.string, - linkedinUrl: PropTypes.string, - instagramUrl: PropTypes.string, -}; - export default GroupMediaIcon; diff --git a/packages/app/components/GroupScreen/GroupTabs.js b/packages/app/components/GroupScreen/GroupTabs.tsx similarity index 86% rename from packages/app/components/GroupScreen/GroupTabs.js rename to packages/app/components/GroupScreen/GroupTabs.tsx index 3192670b..29bacd87 100644 --- a/packages/app/components/GroupScreen/GroupTabs.js +++ b/packages/app/components/GroupScreen/GroupTabs.tsx @@ -1,4 +1,10 @@ -import React, { useRef, useState, useEffect, createRef } from "react"; +import React, { + useRef, + useState, + useEffect, + createRef, + RefObject, +} from "react"; import { View, Text, @@ -7,7 +13,7 @@ import { StyleSheet, } from "react-native"; import { ScrollView } from "react-native-gesture-handler"; -import PropTypes from "prop-types"; +import { GroupTabsProps } from "@app/types/Group"; const BLUE = "#3498DB"; const LIGHT_GRAY = "#E4E4E4"; @@ -20,13 +26,13 @@ const blueViewPosition = new Animated.ValueXY({ y: -1, }); -function GroupTabs(props) { +function GroupTabs(props: GroupTabsProps) { const [isAnimationComplete, setIsAnimationComplete] = useState(true); const { selectTab, tabs, activeTab } = props; // viewRefs.current to access the list // viewRefs.current[index].current to access the view - const viewRefs = useRef([]); + const viewRefs = useRef>>([]); useEffect(() => { // Initialize viewRefs list with a ref for each view @@ -43,7 +49,10 @@ function GroupTabs(props) { getPosition() .then((position) => { Animated.spring(blueViewPosition, { - toValue: { x: position, y: blueViewPosition.y }, + toValue: { + x: position as number, + y: -1, + }, useNativeDriver: true, speed: 100, restSpeedThreshold: 100, @@ -68,7 +77,7 @@ function GroupTabs(props) { return new Promise((resolve, reject) => { for (let index = 0; index < tabs.length; index++) { if (tabs[index] == activeTab && viewRefs.current[index].current) { - viewRefs.current[index].current.measure( + viewRefs.current[index].current?.measure( (x, y, width, height, pageX) => { const tabCenter = pageX + width / 2; const position = tabCenter - blueViewWidth / 2; @@ -129,8 +138,8 @@ function GroupTabs(props) { @@ -148,7 +157,6 @@ const styles = StyleSheet.create({ left: 0, marginTop: -2, right: 0, - transform: blueViewPosition.getTranslateTransform(), width: blueViewWidth, }, bottomBorder: { @@ -156,6 +164,9 @@ const styles = StyleSheet.create({ height: 3, width: "100%", }, + hide: { + opacity: 0, + }, innerTabView: { flexDirection: "row", }, @@ -186,13 +197,4 @@ const styles = StyleSheet.create({ }, }); -GroupTabs.propTypes = { - activeTab: PropTypes.object, - selectTab: PropTypes.func, - size: PropTypes.number, - style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - tabs: PropTypes.arrayOf(PropTypes.object), - testID: PropTypes.string, -}; - export default GroupTabs; diff --git a/packages/app/components/GroupScreen/GroupTag.js b/packages/app/components/GroupScreen/GroupTag.tsx similarity index 87% rename from packages/app/components/GroupScreen/GroupTag.js rename to packages/app/components/GroupScreen/GroupTag.tsx index 221c53fc..287f3142 100644 --- a/packages/app/components/GroupScreen/GroupTag.js +++ b/packages/app/components/GroupScreen/GroupTag.tsx @@ -1,6 +1,6 @@ import React from "react"; import { View, StyleSheet, Text } from "react-native"; -import PropTypes from "prop-types"; +import { GroupTagProps } from "@app/types/Group"; const BLUE = "#3498DB"; const LIGHT_GRAY = "#E4E4E4"; @@ -28,7 +28,7 @@ const styles = StyleSheet.create({ * @param {object} props - Object that contains properties of this component. * @param {string} props.text - Text on the tag. */ -function GroupTag(props) { +function GroupTag(props: GroupTagProps) { return ( {props.text} @@ -36,8 +36,4 @@ function GroupTag(props) { ); } -GroupTag.propTypes = { - text: PropTypes.string, -}; - export default GroupTag; diff --git a/packages/app/components/Screen.js b/packages/app/components/Screen.tsx similarity index 70% rename from packages/app/components/Screen.js rename to packages/app/components/Screen.tsx index 731dadc8..4fe29ce1 100644 --- a/packages/app/components/Screen.js +++ b/packages/app/components/Screen.tsx @@ -1,8 +1,7 @@ import React from "react"; +import { ScreenProps } from "@app/types/Screen"; import { SafeAreaView, StatusBar } from "react-native"; -import PropTypes from "prop-types"; - -function Screen(props) { +function Screen(props: ScreenProps) { const { children, ...rest } = props; return ( @@ -13,8 +12,4 @@ function Screen(props) { ); } -Screen.propTypes = { - children: PropTypes.node -}; - export default Screen; diff --git a/packages/app/components/TextInput.js b/packages/app/components/TextInput.tsx similarity index 56% rename from packages/app/components/TextInput.js rename to packages/app/components/TextInput.tsx index f03230e8..b1bdfa2c 100644 --- a/packages/app/components/TextInput.js +++ b/packages/app/components/TextInput.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useState } from "react"; +import React, { forwardRef, useState, ForwardedRef } from "react"; import { TextInput as RNTextInput, StyleSheet, @@ -10,40 +10,43 @@ import { import EyeOff from "@app/assets/eye-line-off"; import EyeOn from "@app/assets/eye-line-on"; -import PropTypes from "prop-types"; +import { TextInputProps } from "@app/types/TextInput"; const RED = "#f54242"; -const styles = StyleSheet.create({ - container: { - alignItems: "flex-start", - borderRadius: 10, - height: "auto", - width: "100%", - }, - error: { - color: RED, - fontSize: 12, - }, - input: { - flex: 1, - height: "100%", - }, - textField: { - alignItems: "flex-start", - flexDirection: "row", - }, -}); - -const TextInput = forwardRef(function textInput(props, ref) { +const TextInput = forwardRef(function textInput( + props: TextInputProps, + ref: ForwardedRef +) { const [hidePassword, setHidePassword] = useState(props.password); + const styles = StyleSheet.create({ + container: { + alignItems: "flex-start", + borderRadius: 10, + height: "auto", + width: "100%", + }, + error: { + color: RED, + fontSize: 12, + }, + input: { + flex: 1, + height: "100%", + }, + textField: { + alignItems: "flex-start", + borderColor: props.error ? RED : props.borderColor, + flexDirection: "row", + }, + }); + return ( + style={[styles.textField, props.style]}> { + // eslint-disable-next-line @typescript-eslint/no-var-requires const Reanimated = require("react-native-reanimated/mock"); // The mock for `call` immediately calls the callback which is incorrect diff --git a/packages/app/jest.config.js b/packages/app/jest.config.ts similarity index 68% rename from packages/app/jest.config.js rename to packages/app/jest.config.ts index 619357fe..bf365511 100644 --- a/packages/app/jest.config.js +++ b/packages/app/jest.config.ts @@ -1,8 +1,8 @@ -/** @type {import('jest').Config} */ -const config = { +import type {Config} from 'jest'; +const config: Config = { verbose: true, preset: "react-native", - setupFiles: ["./jest-setup.js"], + setupFiles: ["./jest-setup.ts"], transformIgnorePatterns: [ "/node_modules/(?!(jest-)?react-native|@react-native-community|@react-navigation)", ], diff --git a/packages/app/jsconfig.json b/packages/app/jsconfig.json deleted file mode 100644 index e7ad6a53..00000000 --- a/packages/app/jsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": "./", - "paths": { - "@app/*": ["./*"] - } - } -} diff --git a/packages/app/package.json b/packages/app/package.json index 873c3fae..62ec374f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,14 +1,14 @@ { "name": "app", "version": "1.0.0", - "main": "index.js", + "main": "index.ts", "scripts": { "start": "expo start --dev-client", "cold-start": "expo start -c --dev-client", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web", - "dev": "ttab expo start", + "dev": "expo start", "test": "jest" }, "dependencies": { @@ -19,6 +19,8 @@ "@react-navigation/drawer": "^6.5.0", "@react-navigation/native": "^6.0.13", "@react-navigation/native-stack": "^6.9.0", + "@types/react": "~18.0.24", + "@types/react-native": "~0.70.6", "axios": "^0.27.2", "expo": "~47.0.13", "expo-application": "~5.0.1", @@ -29,7 +31,6 @@ "expo-secure-store": "~12.0.0", "expo-status-bar": "~1.4.2", "expo-web-browser": "~12.0.0", - "prop-types": "^15.8.1", "react": "18.1.0", "react-native": "0.70.8", "react-native-gesture-handler": "~2.8.0", @@ -37,16 +38,32 @@ "react-native-safe-area-context": "^4.5.0", "react-native-screens": "~3.18.0", "react-native-svg": "13.4.0", - "react-test-renderer": "^18.2.0" + "react-test-renderer": "^18.2.0", + "typescript": "^5.2.2" }, "devDependencies": { - "@babel/core": "^7.19.3", + "@babel/core": "^7.23.3", "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/preset-env": "^7.23.3", + "@babel/preset-typescript": "^7.23.3", "@testing-library/react-native": "^11.5.0", + "@tsconfig/react-native": "^2.0.3", + "@types/jest": "^29.4.0", + "@types/node": "^20.9.2", + "@types/react": "^18.0.28", + "@types/react-native": "^0.71.2", + "@types/react-test-renderer": "^18.0.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "babel-jest": "^29.7.0", "babel-plugin-module-resolver": "^4.1.0", + "eslint": "^8.54.0", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-native": "4.0.0", - "jest": "^29.3.1" + "jest": "^29.3.1", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" }, "private": true } diff --git a/packages/app/screens/explore/ExploreScreen.js b/packages/app/screens/explore/ExploreScreen.tsx similarity index 100% rename from packages/app/screens/explore/ExploreScreen.js rename to packages/app/screens/explore/ExploreScreen.tsx diff --git a/packages/app/screens/explore/ExploreStack.js b/packages/app/screens/explore/ExploreStack.tsx similarity index 100% rename from packages/app/screens/explore/ExploreStack.js rename to packages/app/screens/explore/ExploreStack.tsx diff --git a/packages/app/screens/feed/FeedDrawer.js b/packages/app/screens/feed/FeedDrawer.tsx similarity index 82% rename from packages/app/screens/feed/FeedDrawer.js rename to packages/app/screens/feed/FeedDrawer.tsx index 20790e8e..3864a950 100644 --- a/packages/app/screens/feed/FeedDrawer.js +++ b/packages/app/screens/feed/FeedDrawer.tsx @@ -1,6 +1,7 @@ import React from "react"; import { createDrawerNavigator, + DrawerContentComponentProps, DrawerContentScrollView, DrawerItemList, } from "@react-navigation/drawer"; @@ -11,11 +12,10 @@ import { useUserContext } from "@app/utils/UserContext"; import Profile from "./feed_tabs/Profile"; import Settings from "./feed_tabs/Settings"; -import PropTypes from "prop-types"; - import Ionicons from "@expo/vector-icons/Ionicons"; +import { FeedStackParamList } from "@app/types/Feed"; -const Feed = createDrawerNavigator(); +const Feed = createDrawerNavigator(); const DARK_BLUE = "darkblue"; const GRAY = "grey"; @@ -23,11 +23,9 @@ const GRAY = "grey"; function FeedDrawer() { return ( }> ({ @@ -66,15 +64,14 @@ function FeedDrawer() { ); } -function CustomDrawerContent(props) { - // eslint-disable-next-line no-unused-vars - const { user, setUser } = useUserContext(); +function CustomDrawerContent(props: DrawerContentComponentProps) { + const { user } = useUserContext(); return ( - + - {user.data.firstName} {user.data.lastName} + {user.data?.firstName} {user.data?.lastName} @@ -89,7 +86,7 @@ const styles = StyleSheet.create({ width: 40, }, drawerButton: { - marginLeft: 18 + marginLeft: 18, }, drawerDisplayName: { color: DARK_BLUE, @@ -107,8 +104,4 @@ const styles = StyleSheet.create({ }, }); -FeedDrawer.propTypes = { - navigation: PropTypes.object, -}; - export default FeedDrawer; diff --git a/packages/app/screens/feed/FeedScreen.js b/packages/app/screens/feed/FeedScreen.tsx similarity index 74% rename from packages/app/screens/feed/FeedScreen.js rename to packages/app/screens/feed/FeedScreen.tsx index 91cf6dc9..904266e3 100644 --- a/packages/app/screens/feed/FeedScreen.js +++ b/packages/app/screens/feed/FeedScreen.tsx @@ -9,6 +9,8 @@ import EventCard from "@app/components/EventCard/EventCard"; import { useUserContext } from "@app/utils/UserContext"; import { logoutUser } from "@app/utils/datalayer"; import { ENDPOINT } from "@app/utils/constants"; +import { EventType } from "@app/types/EventCard"; + import * as SecureStore from "@app/utils/SecureStore"; import { GoogleSignin } from "@react-native-google-signin/google-signin"; @@ -23,7 +25,6 @@ function FeedScreen() { try { // Revoke the refresh token const refreshToken = await SecureStore.getValueFor("refreshToken"); - // eslint-disable-next-line no-unused-vars const response = await axios.post(`${ENDPOINT}/auth/token/revoke`, { refreshToken: refreshToken, }); @@ -42,21 +43,18 @@ function FeedScreen() { }, [setUser]); const getEvents = async () => { - const token = await SecureStore.getValueFor("accessToken"); - const { data: response } = await axios.get(`${ENDPOINT}/events/pages`, { - headers: { - Authorization: token, - }, - }); - - const serializeEvents = response.data.events.map((event) => { - return { - ...event, - key: event.eventId, - }; - }); + try { + const token = await SecureStore.getValueFor("accessToken"); + const { data: response } = await axios.get(`${ENDPOINT}/events/pages`, { + headers: { + Authorization: token ?? "", + }, + }); - setEvents(serializeEvents); + setEvents(response.data.events); + } catch (error) { + console.log(error); + } }; useEffect(() => { @@ -69,23 +67,24 @@ function FeedScreen() { setRefreshing(false); }, []); - const handleRenderItem = useCallback(({ item }) => { + const handleRenderItem = ({ item }: { item: EventType }) => { return ( ); - }, []); + }; return ( <> - Hello, {user.firstName} - + Hello, {user.data?.firstName} + {JSON.stringify(user)} @@ -94,7 +93,7 @@ function FeedScreen() { refreshing={refreshing} data={events} renderItem={handleRenderItem} - keyExtractor={(item) => item.key} + keyExtractor={(item) => item.eventId} /> ); diff --git a/packages/app/screens/feed/FeedStack.js b/packages/app/screens/feed/FeedStack.tsx similarity index 87% rename from packages/app/screens/feed/FeedStack.js rename to packages/app/screens/feed/FeedStack.tsx index fcacad0c..037bf37f 100644 --- a/packages/app/screens/feed/FeedStack.js +++ b/packages/app/screens/feed/FeedStack.tsx @@ -11,7 +11,7 @@ function FeedStack() { - + ); } diff --git a/packages/app/screens/feed/feed_tabs/Profile.js b/packages/app/screens/feed/feed_tabs/Profile.tsx similarity index 78% rename from packages/app/screens/feed/feed_tabs/Profile.js rename to packages/app/screens/feed/feed_tabs/Profile.tsx index 74edad51..c623d459 100644 --- a/packages/app/screens/feed/feed_tabs/Profile.js +++ b/packages/app/screens/feed/feed_tabs/Profile.tsx @@ -1,9 +1,9 @@ import React from "react"; import { View, Button, Text, StyleSheet } from "react-native"; -import PropTypes from "prop-types"; +import { ProfileScreenNavigationProps } from "@app/types/Feed"; // Placeholder profile screen -function Profile({ navigation }) { +function Profile({ navigation }: ProfileScreenNavigationProps) { return ( ; } diff --git a/packages/app/screens/group/tabs/EventsScreen.js b/packages/app/screens/group/tabs/EventsScreen.tsx similarity index 87% rename from packages/app/screens/group/tabs/EventsScreen.js rename to packages/app/screens/group/tabs/EventsScreen.tsx index f52a48cd..3a42ec1d 100644 --- a/packages/app/screens/group/tabs/EventsScreen.js +++ b/packages/app/screens/group/tabs/EventsScreen.tsx @@ -1,7 +1,7 @@ import EventCard from "@app/components/EventCard/EventCard"; import React from "react"; import { View, StyleSheet, Text } from "react-native"; -import PropTypes from "prop-types"; +import { EventScreenProps } from "@app/types/Group"; const DARK_GRAY = "#2C2C2C"; const WHITE = "#F5F5F5"; @@ -21,7 +21,7 @@ const mockData = [ }, ]; -function EventsScreen(props) { +function EventsScreen(props: EventScreenProps) { return ( {mockData.map((section) => ( @@ -45,11 +45,6 @@ function EventsScreen(props) { ); } -EventsScreen.propTypes = { - style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - testID: PropTypes.string, -}; - const styles = StyleSheet.create({ card: { marginBottom: 10, diff --git a/packages/app/screens/group/tabs/LeaderboardScreen.js b/packages/app/screens/group/tabs/LeaderboardScreen.tsx similarity index 62% rename from packages/app/screens/group/tabs/LeaderboardScreen.js rename to packages/app/screens/group/tabs/LeaderboardScreen.tsx index 011ab076..3d9bd455 100644 --- a/packages/app/screens/group/tabs/LeaderboardScreen.js +++ b/packages/app/screens/group/tabs/LeaderboardScreen.tsx @@ -1,6 +1,8 @@ import React from "react"; import { View } from "react-native"; +//import { LeaderboardScreenProps } from "@app/types/Group"; +// props: LeaderboardScreenProps function LeaderboardScreen() { return ; } diff --git a/packages/app/screens/group/tabs/MembersScreen.js b/packages/app/screens/group/tabs/MembersScreen.tsx similarity index 63% rename from packages/app/screens/group/tabs/MembersScreen.js rename to packages/app/screens/group/tabs/MembersScreen.tsx index 8f75fef1..02023fb7 100644 --- a/packages/app/screens/group/tabs/MembersScreen.js +++ b/packages/app/screens/group/tabs/MembersScreen.tsx @@ -1,6 +1,8 @@ import React from "react"; import { View } from "react-native"; +// import { MembersScreenProps } from "@app/types/Group"; +// props: MembersScreenProps function MembersScreen() { return ; } diff --git a/packages/app/screens/group/tabs/NewsletterScreen.js b/packages/app/screens/group/tabs/NewsletterScreen.tsx similarity index 62% rename from packages/app/screens/group/tabs/NewsletterScreen.js rename to packages/app/screens/group/tabs/NewsletterScreen.tsx index b2f7acfa..3e7548c3 100644 --- a/packages/app/screens/group/tabs/NewsletterScreen.js +++ b/packages/app/screens/group/tabs/NewsletterScreen.tsx @@ -1,6 +1,8 @@ import React from "react"; import { View } from "react-native"; +//import { NewsletterScreenProps } from "@app/types/Group"; +// props: NewsletterScreenProps function NewsletterScreen() { return ; } diff --git a/packages/app/screens/landing/ForgotPasswordScreen.js b/packages/app/screens/landing/ForgotPasswordScreen.tsx similarity index 77% rename from packages/app/screens/landing/ForgotPasswordScreen.js rename to packages/app/screens/landing/ForgotPasswordScreen.tsx index 8c67c193..e1f869fb 100644 --- a/packages/app/screens/landing/ForgotPasswordScreen.js +++ b/packages/app/screens/landing/ForgotPasswordScreen.tsx @@ -2,34 +2,32 @@ import React, { useState } from "react"; import { StyleSheet, - Text, KeyboardAvoidingView, TouchableWithoutFeedback, Keyboard, Platform, - Button, } from "react-native"; import Screen from "@app/components/Screen"; import TextInput from "@app/components/TextInput"; +import Button from "@app/components/Button"; -import PropTypes from "prop-types"; +import { ForgotPasswordScreenNavigationProps } from "@app/types/Landing"; +const BLUE = "#0b91e0"; const LIGHT_GRAY = "#ebebeb"; -function ForgotPasswordScreen({ route }) { +function ForgotPasswordScreen({ route }: ForgotPasswordScreenNavigationProps) { const [inputs, setInputs] = useState({ email: route.params?.email ?? "", }); - const [errors, setErrors] = useState({}); + const [errors, setErrors] = useState({ email: null }); - - - const handleOnChange = (inputKey, text) => { + const handleOnChange = (inputKey: string, text: string | null) => { setInputs((prevState) => ({ ...prevState, [inputKey]: text })); }; - const handleError = (inputKey, error) => { + const handleError = (inputKey: string, error: string | null) => { setErrors((prevState) => ({ ...prevState, [inputKey]: error })); }; @@ -58,8 +56,6 @@ function ForgotPasswordScreen({ route }) { - This is {route.params.name} - + placeholder="Email" + onSubmitEditing={() => console.log("Run Forgot Password...")} + /> @@ -106,6 +103,10 @@ const styles = StyleSheet.create({ paddingRight: 20, width: "100%", }, + resetButton: { + backgroundColor: BLUE, + borderColor: BLUE, + }, textInput: { backgroundColor: LIGHT_GRAY, borderWidth: 1, @@ -116,15 +117,10 @@ const styles = StyleSheet.create({ }, }); -const isValidEmail = (email) => { +const isValidEmail = (email: string) => { const emailRE = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i; return email.match(emailRE); }; -ForgotPasswordScreen.propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, -}; - export default ForgotPasswordScreen; diff --git a/packages/app/screens/landing/LandingScreen.js b/packages/app/screens/landing/LandingScreen.tsx similarity index 85% rename from packages/app/screens/landing/LandingScreen.js rename to packages/app/screens/landing/LandingScreen.tsx index ef628168..c50f244c 100644 --- a/packages/app/screens/landing/LandingScreen.js +++ b/packages/app/screens/landing/LandingScreen.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef } from "react"; -import axios from "axios"; +import axios, { AxiosError } from "axios"; import { StyleSheet, View, @@ -9,23 +9,23 @@ import { Keyboard, TouchableOpacity, Platform, + TextInput as RNTextInput, } from "react-native"; -import * as WebBrowser from "expo-web-browser"; - import Button from "@app/components/Button"; import Screen from "@app/components/Screen"; import TextInput from "@app/components/TextInput"; import GoogleIcon from "@app/assets/google-icon"; import { useUserContext } from "@app/utils/UserContext"; +import * as WebBrowser from "expo-web-browser"; + import { ENDPOINT } from "@app/utils/constants"; import * as SecureStore from "@app/utils/SecureStore"; import { useGoogleLogin } from "@app/utils/useGoogleLogin"; - -import PropTypes from "prop-types"; +import { LandingScreenNavigationProps } from "@app/types/Landing"; WebBrowser.maybeCompleteAuthSession(); @@ -34,7 +34,7 @@ const DARK_GRAY = "#a3a3a3"; const GRAY = "#c4c4c4"; const LIGHT_GRAY = "#ebebeb"; -function LandingScreen({ navigation, route }) { +function LandingScreen({ navigation, route }: LandingScreenNavigationProps) { const { user, setUser } = useUserContext(); // State to change the variable with the TextInput @@ -42,7 +42,7 @@ function LandingScreen({ navigation, route }) { email: route.params?.email ?? "", password: "", }); - const [errors, setErrors] = useState({}); + const [errors, setErrors] = useState({ email: null, password: null }); const validateInput = () => { let isValid = true; @@ -72,6 +72,7 @@ function LandingScreen({ navigation, route }) { const login = async () => { try { + console.log(`${ENDPOINT}/auth/local`); const response = await axios.post(`${ENDPOINT}/auth/local`, { email: inputs.email, password: inputs.password, @@ -89,29 +90,31 @@ function LandingScreen({ navigation, route }) { console.log(response?.data.message); } } catch (error) { - const responseData = error.response.data; - if ( - responseData.data && - (responseData.data.email === "A user with that email does not exist." || - responseData.data.password === "Incorrect password") - ) { - handleError("email", "Invalid email or password."); - } else { - console.log(JSON.stringify(error)); + if (error instanceof AxiosError) { + const responseData = error.response?.data; + if ( + responseData.data && + (responseData.data.email === "A user with that email does not exist." || + responseData.data.password === "Incorrect password") + ) { + handleError("email", "Invalid email or password."); + } else { + console.log(JSON.stringify(error)); + } } } }; - const handleOnChange = (inputKey, text) => { + const handleOnChange = (inputKey: string, text: string | null) => { setInputs((prevState) => ({ ...prevState, [inputKey]: text })); }; - const handleError = (inputKey, error) => { + const handleError = (inputKey: string, error: string | null) => { setErrors((prevState) => ({ ...prevState, [inputKey]: error })); }; // Keeps a reference to help switch from Username input to Password input - const refPasswordInput = useRef(); + const refPasswordInput = useRef(null); return ( @@ -137,7 +140,7 @@ function LandingScreen({ navigation, route }) { error={errors.email} placeholder="Email" onSubmitEditing={() => { - refPasswordInput.current.focus(); + refPasswordInput.current?.focus(); }} /> @@ -174,11 +177,9 @@ function LandingScreen({ navigation, route }) { onPress={() => { validateInput(); }} - underlayColor="#0e81c4" fontColor="#ffffff" fontWeight="bold" style={[styles.loginButton, styles.component]} - textStyle={styles.boldText} /> @@ -186,8 +187,7 @@ function LandingScreen({ navigation, route }) {