diff --git a/assets/groups/facebook.svg b/assets/groups/facebook.svg new file mode 100644 index 00000000..0ab81d19 --- /dev/null +++ b/assets/groups/facebook.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/groups/index.ts b/assets/groups/index.ts new file mode 100644 index 00000000..fd402523 --- /dev/null +++ b/assets/groups/index.ts @@ -0,0 +1,32 @@ +import { DataSourceParam } from "@shopify/react-native-skia" +import whatsapp from "./whatsapp.svg" +import facebook from "./facebook.svg" +import telegram from "./telegram.svg" + +/** + * list of tray icons + */ +export const platformIconList = ["telegram", "whatsapp", "facebook"] as const + +export type PlatformIcon = typeof platformIconList[number] + +export const platformIcons: Record< + PlatformIcon, + { svg: DataSourceParam; width: number; heigth: number } +> = { + whatsapp: { + svg: whatsapp, + width: 24, + heigth: 24, + }, + telegram: { + svg: telegram, + width: 24, + heigth: 24, + }, + facebook: { + svg: facebook, + width: 25, + heigth: 24, + }, +} \ No newline at end of file diff --git a/assets/groups/telegram.svg b/assets/groups/telegram.svg new file mode 100644 index 00000000..bbc6a5e4 --- /dev/null +++ b/assets/groups/telegram.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/groups/whatsapp.svg b/assets/groups/whatsapp.svg new file mode 100644 index 00000000..5f7a5387 --- /dev/null +++ b/assets/groups/whatsapp.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/api/groups.ts b/src/api/groups.ts new file mode 100644 index 00000000..b2dbc8f7 --- /dev/null +++ b/src/api/groups.ts @@ -0,0 +1,59 @@ +import { HttpClient, RequestOptions } from "./HttpClient" + +/* eslint-disable @typescript-eslint/naming-convention */ + +export interface GroupOptions { + name?: string + year?: string + degree?: string + type?: string + platform?: string + language?: string + office?: string +} + +export interface Group { + class: string + office: string + id: string + degree?: string + school?: string + link_id: string + language: string + type_?: string + year: string | null //probably I should use | null evreywhere? + platform: string + permanent_id?: number + last_updated?: string + link_is_working?: string + members?: string +} + +const client = HttpClient.getInstance() + +/** + * Collection of endpoints related to Groups. + */ +export const groups = { + /** + * Retrieves groups from PoliNetwork server. + * Check {@link GroupOptions} for additional parameters. + */ + async get(groupsOptions?: GroupOptions, options?: RequestOptions) { + const response = await client.poliNetworkInstance.get<{ + groups: Group[] + }>("/v1/groups", { + ...options, + params: { + name: groupsOptions?.name, + year: groupsOptions?.year, + degree: groupsOptions?.degree, + type: groupsOptions?.type, + platform: groupsOptions?.platform, + language: groupsOptions?.language, + office: groupsOptions?.office, + }, + }) + return response.data.groups + }, +} diff --git a/src/api/index.ts b/src/api/index.ts index 3fd94a14..ff3c70e3 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,5 +1,6 @@ import { articles } from "./articles" import { auth } from "./auth" +import { groups } from "./groups" import { tags } from "./tags" import { timetable } from "./timetable" import { user } from "./user" @@ -27,4 +28,5 @@ export const api = { tags, timetable, user, + groups, } diff --git a/src/components/Settings/SettingsScroll.tsx b/src/components/ContentWrapperScroll.tsx similarity index 68% rename from src/components/Settings/SettingsScroll.tsx rename to src/components/ContentWrapperScroll.tsx index c99a15b1..b238b91c 100644 --- a/src/components/Settings/SettingsScroll.tsx +++ b/src/components/ContentWrapperScroll.tsx @@ -7,9 +7,12 @@ import { usePalette } from "utils/colors" /** * General component useful for pages with a scrollable content. * It provides a navbar and a scrollview with margin and rounded corners. + * Default margin Top is 86 (proper margin for Settings Page) */ -export const SettingsScroll: FC<{ - title: string +export const ContentWrapperScroll: FC<{ + children: React.ReactNode + + title?: string /** * Remove the navbar from the bottom of the page. */ @@ -18,8 +21,7 @@ export const SettingsScroll: FC<{ * Props for the navbar, see {@link NavBar} */ navbarOptions?: NavbarProps - - children: React.ReactNode + marginTop?: number }> = props => { const { background, isLight, primary } = usePalette() @@ -32,30 +34,33 @@ export const SettingsScroll: FC<{ backgroundColor: isLight ? primary : background, }} > - - - {props.title} - - + + {props.title} + + + )} + {props.children} - {navbar ? : null} ) diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx index e2512791..3e7c2454 100644 --- a/src/components/Divider.tsx +++ b/src/components/Divider.tsx @@ -1,19 +1,25 @@ import React, { FC } from "react" -import { View } from "react-native" +import { View, ViewStyle } from "react-native" export interface DividerProps { color?: string height?: number + width?: number + style?: ViewStyle } export const Divider: FC = props => { return ( ) } diff --git a/src/components/Groups/AnimatedLine.tsx b/src/components/Groups/AnimatedLine.tsx new file mode 100644 index 00000000..98045b11 --- /dev/null +++ b/src/components/Groups/AnimatedLine.tsx @@ -0,0 +1,54 @@ +import React, { FC } from "react" +import { Animated, Easing, ViewStyle } from "react-native" +import { usePalette } from "utils/colors" + +export interface AnimatedLineProps { + /** + * animate when this value changes + */ + mounted: boolean + color?: string + height?: number + width?: number + style?: ViewStyle +} + +export const AnimatedLine: FC = props => { + const { isLight } = usePalette() + + const { current: widthAnim } = React.useRef( + new Animated.Value(1) + ) + + React.useEffect(() => { + if (props.mounted) { + Animated.timing(widthAnim, { + toValue: 250, + duration: 300, + easing: Easing.ease, + useNativeDriver: false, + }).start() + } else { + Animated.timing(widthAnim, { + toValue: 0, + duration: 300, + easing: Easing.ease, + useNativeDriver: false, + }).start() + } + }, [props.mounted]) + + return ( + + ) +} diff --git a/src/components/Groups/AnimatedPoliSearchBar.tsx b/src/components/Groups/AnimatedPoliSearchBar.tsx new file mode 100644 index 00000000..a990a25e --- /dev/null +++ b/src/components/Groups/AnimatedPoliSearchBar.tsx @@ -0,0 +1,29 @@ +import { PoliSearchBar } from "components/Home" +import React, { FC, useState } from "react" +import { View, ViewStyle } from "react-native" +import { AnimatedLine } from "./AnimatedLine" + +export interface AnimatedPoliSearchBarProps { + onSearch: (val: string) => void + style?: ViewStyle +} + +export const AnimatedPoliSearchBar: FC = props => { + const [isSearching, setIsSearching] = useState(false) + return ( + + { + props.onSearch(val) + if (val !== "") { + setIsSearching(true) + } else if (isSearching === true) { + setIsSearching(false) + } + }} + style={{ marginTop: 0, marginBottom: 0 }} + /> + + + ) +} diff --git a/src/components/Groups/Filters.tsx b/src/components/Groups/Filters.tsx new file mode 100644 index 00000000..a2fec793 --- /dev/null +++ b/src/components/Groups/Filters.tsx @@ -0,0 +1,196 @@ +import React, { FC, useState } from "react" +import { View } from "react-native" +import { OutlinedButton } from "./OutlinedButton" +import { StyleSheet } from "react-native" +import { ModalSelection, SelectTile } from "components/Settings" +import { getNameFromMode, ValidModalType } from "utils/groups" + +export interface FiltersProps { + filters: Filters + onFilterChange: (filters: Filters) => void +} + +export interface Filters { + year?: string + course?: string + platform?: string + type?: string +} + +interface ModalItemList { + itemsToShow: string[] + itemsToSave: string[] +} + +const yearsList: ModalItemList = { + itemsToShow: [ + "2022/2023", + "2021/2022", + "2020/2021", + "2019/2020", + "2018/2019", + ], + itemsToSave: [ + "2022/2023", + "2021/2022", + "2020/2021", + "2019/2020", + "2018/2019", + ], +} +const coursesList: ModalItemList = { + itemsToShow: ["Triennale", "Magistrale", "Ciclo unico"], + itemsToSave: ["LT", "LM", "LU"], +} + +const typesList: ModalItemList = { + itemsToShow: ["Scuola", "Corso", "Extra"], + itemsToSave: ["S", "C", "E"], +} + +const platformsList: ModalItemList = { + itemsToShow: ["Whatsapp", "Facebook", "Telegram"], + itemsToSave: ["WA", "FB", "TG"], +} + + +export const Filters: FC = props => { + //show or hide modal + const [isModalShowing, setIsModalShowing] = useState(false) + //type of modal: year - type - course - platform + const [modalMode, setModalMode] = useState("year") + //items to show inside modal + const [modalItems, setModalItems] = useState(yearsList) + //currently selected item inside modal + const [selectedItem, setSelectedItem] = useState( + undefined + ) + + //reset state on "reset" + const reset = () => { + props.onFilterChange({}) + } + + return ( + + + { + setModalMode("year") + setModalItems(yearsList) + setSelectedItem(props.filters.year) + setIsModalShowing(true) + }} + /> + { + setModalMode("course") + setModalItems(coursesList) + setSelectedItem(props.filters.course) + setIsModalShowing(true) + }} + /> + { + setModalMode("type") + setModalItems(typesList) + setSelectedItem(props.filters.type) + setIsModalShowing(true) + }} + /> + { + setModalMode("platform") + setModalItems(platformsList) + setSelectedItem(props.filters.platform) + setIsModalShowing(true) + }} + /> + + + { + setIsModalShowing(false) + }} + onOK={() => { + if (modalMode === "course") { + props.onFilterChange({ + ...props.filters, + course: selectedItem, + }) + } else if (modalMode === "platform") { + props.onFilterChange({ + ...props.filters, + platform: selectedItem, + }) + } else if (modalMode === "year") { + props.onFilterChange({ + ...props.filters, + year: selectedItem, + }) + } else if (modalMode === "type") { + props.onFilterChange({ + ...props.filters, + type: selectedItem, + }) + } + setIsModalShowing(false) + }} + > + { + setSelectedItem(undefined) + }} + /> + {modalItems?.itemsToShow.map((itemName, index) => { + return ( + { + setSelectedItem(modalItems.itemsToSave[index]) + }} + /> + ) + })} + + + ) +} + +const styles = StyleSheet.create({ + buttonCustomMargin: { + marginRight: 4, + marginLeft: 4, + marginBottom: 8, + }, +}) diff --git a/src/components/Groups/GroupTile.tsx b/src/components/Groups/GroupTile.tsx new file mode 100644 index 00000000..bfd5aac3 --- /dev/null +++ b/src/components/Groups/GroupTile.tsx @@ -0,0 +1,105 @@ +import { + Canvas, + DataSourceParam, + ImageSVG, + useSVG, +} from "@shopify/react-native-skia" +import { Divider } from "components/Divider" +import { BodyText } from "components/Text" +import React, { FC } from "react" +import { Pressable, View } from "react-native" +import { usePalette } from "utils/colors" + +export interface GroupTileProps { + text?: string + icon?: { svg: DataSourceParam; width: number; heigth: number } + members?: string + onClick?: () => void +} + +export const GroupTile: FC = props => { + const { isLight } = usePalette() + + const iconSvg = useSVG(props.icon?.svg) + + return ( + + + + + {props.icon && iconSvg && ( + + + {iconSvg && ( + + )} + + + )} + + + + {props.text} + + + {props.members && ( + + {props.members} members + + )} + + + + + + ) +} diff --git a/src/components/Groups/ModalGroup.tsx b/src/components/Groups/ModalGroup.tsx new file mode 100644 index 00000000..d52e2680 --- /dev/null +++ b/src/components/Groups/ModalGroup.tsx @@ -0,0 +1,152 @@ +import React, { FC } from "react" +import { View, Modal, StyleSheet, Pressable } from "react-native" +import { usePalette } from "utils/colors" +import { ButtonCustom } from "components/Settings" +import { Canvas, ImageSVG, useSVG } from "@shopify/react-native-skia" +import { deleteSvg as icon } from "assets/modal" +import { Group } from "api/groups" + +export interface ModalGroupProps { + /** + * content of the modal + */ + children: React.ReactNode + + /** + * whether ot not to show the modal + */ + isShowing: boolean + /** + * this function hides the modal by changing the state in the parent component + */ + onClose: () => void + + /** + * function called when button "JOIN GROUP" is pressed + */ + onJoin: (group?: Group) => void + + group?: Group + /** + * modal wrapper height, specify if height is fixed + */ + height?: number +} + +/** + * Custom Modal Component for Groups Page. + * + */ +export const ModalGroup: FC = props => { + const { backgroundSecondary, modalBarrier, isLight } = usePalette() + + const deleteSvg = useSVG(icon.svg) + return ( + //TODO: animationType fade or slide? + + + + props.onClose()} + > + + + {deleteSvg && ( + + )} + + + + + {props.children} + + props.onJoin(props.group)} + /> + + + + + + ) +} + +const styles = StyleSheet.create({ + pageWrapper: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + contentWrapper: { + flexDirection: "column", + justifyContent: "space-between", + width: 320, + borderRadius: 12, + marginHorizontal: 15, + + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 3, + }, + shadowOpacity: 0.27, + shadowRadius: 4.65, + elevation: 6, + }, + title: { + fontSize: 32, + fontWeight: "900", + }, +}) diff --git a/src/components/Groups/ModalGroupItem.tsx b/src/components/Groups/ModalGroupItem.tsx new file mode 100644 index 00000000..c6eac75b --- /dev/null +++ b/src/components/Groups/ModalGroupItem.tsx @@ -0,0 +1,98 @@ +import { BodyText } from "components/Text" +import React, { FC } from "react" +import { View } from "react-native" +import { usePalette } from "utils/colors" +import { Group } from "api/groups" +import { choosePlatformIcon } from "utils/groups" +import { Canvas, ImageSVG, useSVG } from "@shopify/react-native-skia" + +export interface ModalGroupItemProps { + /** + * ResetButton + */ + group?: Group +} + +export const ModalGroupItem: FC = props => { + const { isLight } = usePalette() + const icon = choosePlatformIcon(props.group?.platform) + const iconSvg = useSVG(icon?.svg) + + const scaleFactor = 2.5 + + return ( + + + {icon && iconSvg && ( + + {iconSvg && ( + + )} + + )} + + + {props.group?.class} + + + {props.group?.members && ( + + {props.group.members} members + + )} + + {props.group?.year} + + + + ) +} diff --git a/src/components/Groups/OutlinedButton.tsx b/src/components/Groups/OutlinedButton.tsx new file mode 100644 index 00000000..df89c456 --- /dev/null +++ b/src/components/Groups/OutlinedButton.tsx @@ -0,0 +1,69 @@ +import { BodyText } from "components/Text" +import React, { FC } from "react" +import { Pressable, StyleProp, ViewStyle } from "react-native" +import { usePalette } from "utils/colors" +import { StyleSheet } from "react-native" +export interface OutlinedButtonProps { + text?: string + /** + * ResetButton + */ + isSpecial?: boolean + isSelected?: boolean + onPress?: () => void + buttonStyle?: StyleProp +} + +export const OutlinedButton: FC = props => { + const { isLight } = usePalette() + return ( + + + {props.text} + + + ) +} + +const styles = StyleSheet.create({ + button: { + borderWidth: 2, + height: 32, + minWidth: 100, + borderRadius: 60, + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + }, +}) diff --git a/src/components/Groups/PageWrapper.tsx b/src/components/Groups/PageWrapper.tsx new file mode 100644 index 00000000..8c46e51e --- /dev/null +++ b/src/components/Groups/PageWrapper.tsx @@ -0,0 +1,56 @@ +import React, { FC } from "react" +import { View, ViewStyle } from "react-native" +import { NavBar, NavbarProps } from "components/NavBar" +import { usePalette } from "utils/colors" + +/** + * Groups page Wrapper + */ +export const PageWrapper: FC<{ + children: React.ReactNode + + title?: string + /** + * Remove the navbar from the bottom of the page. + */ + hideNavbar?: boolean + /** + * Props for the navbar, see {@link NavBar} + */ + navbarOptions?: NavbarProps + marginTop?: number + style?: ViewStyle +}> = props => { + const { background, isLight, primary } = usePalette() + + const navbar = !props.hideNavbar + + return ( + + + {props.children} + + {navbar ? : null} + + ) +} diff --git a/src/components/Home/MainMenu.tsx b/src/components/Home/MainMenu.tsx index ebfc923f..68766c4c 100644 --- a/src/components/Home/MainMenu.tsx +++ b/src/components/Home/MainMenu.tsx @@ -162,7 +162,9 @@ export const MainMenu: FC<{ filter?: string }> = ({ filter }) => { if (isDeleting) setIsDeleting(false) if (buttonIcon.id === 9) setModalVisible(true) // TODO: actual navigation - if (!isDeleting && buttonIcon.id !== 9) { + if (buttonIcon.id === 5) { + navigate("Groups") + } else if (!isDeleting && buttonIcon.id !== 9) { navigate("Error404") } }} diff --git a/src/components/Home/PoliSearchBar.tsx b/src/components/Home/PoliSearchBar.tsx index 4de04f47..7e35fd6f 100644 --- a/src/components/Home/PoliSearchBar.tsx +++ b/src/components/Home/PoliSearchBar.tsx @@ -1,5 +1,12 @@ import React, { FC, useEffect, useState, useRef } from "react" -import { TextInput, Animated, Pressable } from "react-native" +import { + TextInput, + Animated, + Pressable, + StyleProp, + ViewStyle, + Keyboard, +} from "react-native" import { usePalette } from "utils/colors" import { Canvas, ImageSVG, useSVG } from "@shopify/react-native-skia" import searchLight from "assets/menu/searchLight.svg" @@ -10,7 +17,8 @@ import searchDark from "assets/menu/searchDark.svg" */ export const PoliSearchBar: FC<{ onChange: (searchKey: string) => void -}> = ({ onChange }) => { + style?: StyleProp +}> = ({ onChange, style }) => { const { fieldBackground, fieldText, bodyText, isLight } = usePalette() const svg = useSVG(isLight ? searchLight : searchDark) @@ -18,6 +26,20 @@ export const PoliSearchBar: FC<{ const [isFocused, setIsFocused] = useState(false) const shadowAnim = useRef(new Animated.Value(0)).current const inputText = useRef(null) + + useEffect(() => { + const keyboardDidHideListener = Keyboard.addListener( + "keyboardDidHide", + () => { + inputText.current?.blur() + } + ) + + return () => { + keyboardDidHideListener.remove() + } + }, []) + useEffect(() => { const duration = 100 if (isFocused) @@ -36,26 +58,29 @@ export const PoliSearchBar: FC<{ return ( void - buttonStyle?: ViewStyle + style?: ViewStyle } /** * Custom button component. Specify param `light` to select button type @@ -26,14 +26,14 @@ export const ButtonCustom: FC = props => { { backgroundColor: props.light ? isLight - ? palette.lighter - : palette.darker + ? (palette.lighter) + : ( palette.darker) : isLight ? palette.darker : palette.lighter, minWidth: 130, }, - props.buttonStyle, + props.style, ]} onPress={props.onPress} > diff --git a/src/components/Settings/CareerTile.tsx b/src/components/Settings/CareerTile.tsx index a3e2b1aa..161c9ee6 100644 --- a/src/components/Settings/CareerTile.tsx +++ b/src/components/Settings/CareerTile.tsx @@ -31,7 +31,7 @@ export const CareerTile: FC = props => { diff --git a/src/components/Settings/ModalSettings.tsx b/src/components/Settings/ModalSelection.tsx similarity index 93% rename from src/components/Settings/ModalSettings.tsx rename to src/components/Settings/ModalSelection.tsx index d7d52e4a..e7b609cb 100644 --- a/src/components/Settings/ModalSettings.tsx +++ b/src/components/Settings/ModalSelection.tsx @@ -4,7 +4,7 @@ import { Text } from "components/Text" import { usePalette } from "utils/colors" import { ButtonCustom } from "./ButtonCustom" -export interface ModalCustomSettingsProps { +export interface ModalSelectionProps { /** * content of the modal */ @@ -26,23 +26,19 @@ export interface ModalCustomSettingsProps { */ onOK: () => void - /** - * input value of `onOk` function. - * Usually the current state value of a {@link RadioButtonGroup} - */ - selectedValue: string - /** * modal wrapper height, specify if height is fixed */ height?: number } +// ? maybe should move this out of Settings folder ? + /** * Custom Modal Component with two buttons at the bottom. * */ -export const ModalCustomSettings: FC = props => { +export const ModalSelection: FC = props => { const { backgroundSecondary, homeBackground, modalBarrier, isLight } = usePalette() diff --git a/src/components/Settings/index.ts b/src/components/Settings/index.ts index 4646d8f1..b1393746 100644 --- a/src/components/Settings/index.ts +++ b/src/components/Settings/index.ts @@ -3,8 +3,8 @@ export * from "./ButtonCustom" export * from "./CareerTile" export * from "./SettingTile" export * from "./SelectTile" -export * from "./SettingsScroll" +export * from "../ContentWrapperScroll" export * from "./UserDetailsTile" -export * from "./ModalSettings" +export * from "./ModalSelection" export * from "./UserAnonymousTile" export * from "./CareerColumn" diff --git a/src/components/Text/BodyText.tsx b/src/components/Text/BodyText.tsx index b9790831..402248c6 100644 --- a/src/components/Text/BodyText.tsx +++ b/src/components/Text/BodyText.tsx @@ -22,7 +22,9 @@ export const BodyText: FC = props => { fontFamily: fontWeight === "900" ? "Roboto_900Black" - : fontWeight === "bold" || fontWeight === "700" + : fontWeight === "bold" || + fontWeight === "700" || + fontWeight === "600" ? "Roboto_700Bold" : fontWeight === "300" ? "Roboto_300Light" diff --git a/src/navigation/MainStackNavigator.tsx b/src/navigation/MainStackNavigator.tsx index 2f66da87..9054b8b9 100644 --- a/src/navigation/MainStackNavigator.tsx +++ b/src/navigation/MainStackNavigator.tsx @@ -10,6 +10,7 @@ import { Home } from "pages/Home" import { Article } from "pages/ArticleDetails" import { NewsList } from "pages/NewsList" import { Error404 } from "pages/Error404" +import { Groups } from "pages/Groups" // eslint-disable-next-line @typescript-eslint/naming-convention const MainStackNavigator = createStackNavigator() @@ -21,6 +22,7 @@ export const MainStack: FC = () => { + ) } diff --git a/src/navigation/NavigationTypes.ts b/src/navigation/NavigationTypes.ts index ade07f25..6db61600 100644 --- a/src/navigation/NavigationTypes.ts +++ b/src/navigation/NavigationTypes.ts @@ -44,6 +44,7 @@ export type MainStackNavigatorParams = { Article: { article: Article } NewsList: { categoryName: string } Error404: undefined + Groups: undefined } export type SettingsStackNavigatorParams = { diff --git a/src/pages/Groups.tsx b/src/pages/Groups.tsx new file mode 100644 index 00000000..38fbdc27 --- /dev/null +++ b/src/pages/Groups.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useState } from "react" +import { MainStackScreen } from "navigation/NavigationTypes" +import { FlatList, Linking, View } from "react-native" +import { Title } from "components/Text" +import { Filters } from "components/Groups/Filters" +import { api, RetryType } from "api" +import { Group } from "api/groups" +import { useMounted } from "utils/useMounted" +import { + choosePlatformIcon, + createGroupLink, + orderByMostRecentYear, +} from "utils/groups" + +import { AnimatedPoliSearchBar } from "components/Groups/AnimatedPoliSearchBar" +import { GroupTile } from "components/Groups/GroupTile" +import { PageWrapper } from "components/Groups/PageWrapper" +import { ModalGroup } from "components/Groups/ModalGroup" +import { ModalGroupItem } from "components/Groups/ModalGroupItem" + +const deltaTime = 100 //ms +let searchTimeout: NodeJS.Timeout + +export const Groups: MainStackScreen<"Groups"> = () => { + const [search, setSearch] = useState("") + + const [filters, setFilters] = useState({}) + + const [groups, setGroups] = useState([]) + + const [isModalShowing, setIsModalShowing] = useState(false) + + const [modalGroup, setModalGroup] = useState(undefined) + + //tracking first render + const isMounted = useMounted() + + /** + * Api search request. + */ + const searchGroups = async () => { + if (isMounted) { + if (search.length < 3) { + setGroups([]) + return + } + try { + //update last time search + const response = await api.groups.get( + { + name: search.trimEnd(), + year: filters.year, + platform: filters.platform, + type: filters.type, + degree: filters.course, + }, + { maxRetries: 1, retryType: RetryType.RETRY_N_TIMES } + ) + setGroups(response) + + //reset need searching for next render + } catch (error) { + console.log(error) + } + } + } + + useEffect(() => { + clearTimeout(searchTimeout) + searchTimeout = setTimeout(() => { + void searchGroups() + }, deltaTime) + }, [search]) + + //if filters are applied after search, search again + useEffect(() => { + if (isMounted && groups) void searchGroups() + }, [filters]) + + const orderedGroups = + filters.year === undefined ? orderByMostRecentYear(groups) : groups + + return ( + + + Gruppi Corsi + setSearch(val)} + style={{ marginTop: 36, marginBottom: 22 }} + /> + setFilters(filters)} + filters={filters} + /> + + ( + { + setModalGroup(item) + setIsModalShowing(true) + }} + icon={choosePlatformIcon(item.platform)} + /> + )} + /> + setIsModalShowing(false)} + onJoin={async (group?: Group) => { + if (!group?.link_id) { + return + } + + const link = createGroupLink(group.link_id, group.platform) + // Checking if the link is supported for links with custom URL scheme. + const supported = await Linking.canOpenURL(link) + + if (supported) { + // Opening the link with some app + await Linking.openURL(link) + } + }} + > + + + + ) +} diff --git a/src/pages/settings/Help.tsx b/src/pages/settings/Help.tsx index cb1c5d78..46df9bc9 100644 --- a/src/pages/settings/Help.tsx +++ b/src/pages/settings/Help.tsx @@ -1,7 +1,7 @@ import React from "react" import { View } from "react-native" import { SettingsStackScreen } from "navigation/NavigationTypes" -import { SettingsScroll } from "components/Settings/SettingsScroll" +import { ContentWrapperScroll } from "components/ContentWrapperScroll" import { SettingTile } from "components/Settings/SettingTile" import { SettingOptions } from "utils/settings" @@ -28,12 +28,12 @@ export const settingsList: SettingOptions[] = [ */ export const Help: SettingsStackScreen<"Help"> = () => { return ( - + {settingsList.map((setting, index) => { return })} - + ) } diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index 47953055..4391640c 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -1,12 +1,12 @@ import React, { useContext, useState } from "react" import { View } from "react-native" import { SettingsStackScreen, useNavigation } from "navigation/NavigationTypes" -import { SettingsScroll } from "components/Settings" +import { ContentWrapperScroll } from "components/Settings" import { Divider } from "components/Divider" import { SettingTile } from "components/Settings" import { settingsIcons } from "assets/settings" import { UserDetailsTile } from "components/Settings" -import { ModalCustomSettings } from "components/Settings" +import { ModalSelection } from "components/Settings" import { CareerTile } from "components/Settings" import { SelectTile } from "components/Settings" import { UserAnonymousTile } from "components/Settings" @@ -86,7 +86,7 @@ export const SettingsPage: SettingsStackScreen<"Settings"> = () => { return ( - + {loggedIn ? ( ) : ( @@ -108,12 +108,11 @@ export const SettingsPage: SettingsStackScreen<"Settings"> = () => { {settingsList.map((setting, index) => { return })} - + - { //restore real theme value setSelectedTheme(theme) @@ -136,11 +135,10 @@ export const SettingsPage: SettingsStackScreen<"Settings"> = () => { /> ) })} - - + { //restore selectedCareer to career if (career) setSelectedCareer(career) @@ -169,7 +167,7 @@ export const SettingsPage: SettingsStackScreen<"Settings"> = () => { ) })} - + ) } diff --git a/src/utils/groups.ts b/src/utils/groups.ts new file mode 100644 index 00000000..0d17fff8 --- /dev/null +++ b/src/utils/groups.ts @@ -0,0 +1,94 @@ +import { Group } from "api/groups" +import { platformIcons } from "assets/groups" +/** + * return groups ordered by most recent year using a bubble sort algorithm + * see {@link Groups} Page + */ +export function orderByMostRecentYear(groups: Group[]) { + let hasChanged: boolean + try { + do { + hasChanged = false + for (let n = 0; n < groups.length - 1; n++) { + if ( + compareBiYear(groups[n].year, groups[n + 1].year) === true + ) { + //swap + const temp = groups[n] + groups[n] = groups[n + 1] + groups[n + 1] = temp + hasChanged = true + } + } + } while (hasChanged === true) + } catch (error) { + console.log(error) + } + return groups +} + +function compareBiYear(first: string | null, second: string | null) { + //apparently null != undefined :( code breaks if I use undefined instead of null + if ((first === "?/?" || first === null) && second !== null) { + return true + } else if (second === null || first === null) { + return false + } + //standard year format "2021/2022" + const regexStandard = /^\d{4}\/\d{4}$/ + //for inconsistencies in the db ex "2021/22" + const regexNonStandard = /^\d{4}\/\d{2}$/ + if ( + (regexStandard.test(first) && regexStandard.test(second)) || + (regexNonStandard.test(first) && regexNonStandard.test(second)) + ) { + if (parseInt(first.substring(5)) < parseInt(second.substring(5))) { + return true + } + } else if (regexNonStandard.test(first) && regexStandard.test(second)) { + if (parseInt(first.substring(5)) < parseInt(second.substring(7))) { + return true + } + } else if (regexStandard.test(first) && regexNonStandard.test(second)) { + if (parseInt(first.substring(7)) < parseInt(second.substring(5))) { + return true + } + } + return false +} + +export type ValidModalType = "year" | "course" | "type" | "platform" + +export const getNameFromMode = (mode: ValidModalType) => { + if (mode === "year") { + return "Anno" + } else if (mode === "course") { + return "Corso" + } else if (mode === "platform") { + return "Piattaforma" + } else { + return "Tipo" + } +} + +export function createGroupLink(idLink: string, platform: string) { + if (platform === "TG") { + return `https://t.me/joinchat/${idLink}` + } else if (platform === "WA") { + return `https://chat.whatsapp.com/${idLink}` + } else { + return `https://www.facebook.com/groups/${idLink}` + } +} + + +export function choosePlatformIcon(platform? : string){ + if(platform === "TG"){ + return platformIcons.telegram + }else if(platform ==="FB"){ + return platformIcons.facebook + }else if(platform === "WA"){ + return platformIcons.whatsapp + } + return undefined +} \ No newline at end of file diff --git a/src/utils/useMounted.ts b/src/utils/useMounted.ts new file mode 100644 index 00000000..3c36dc4b --- /dev/null +++ b/src/utils/useMounted.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react" + +/** + * useful hook to keep track of first render in multiple useEffects + * + * from https://stackoverflow.com/questions/57240169/skip-first-useeffect-when-there-are-multiple-useeffects + * + * @example + * ```ts + * const [valueFirst, setValueFirst] = useState(0) + * const [valueSecond, setValueSecond] = useState(0) + * + * const isMounted = useMounted() + * + * //1st effect which should run whenever valueFirst change except + * //first time + * React.useEffect(() => { + * if (isMounted) { + * console.log("valueFirst ran") + * } + * + * }, [valueFirst]) + * + * + * //2nd effect which should run whenever valueFirst change except + * //first time + * React.useEffect(() => { + * if (isMounted) { + * console.log("valueSecond ran") + * } + * + * }, [valueSecond]) + * + * ``` + */ +export function useMounted() { + const [isMounted, setIsMounted] = useState(false) + + useEffect(() => { + setIsMounted(true) + }, []) + return isMounted +}