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 && (
+
+
+
+ )}
+
+
+
+ {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()}
+ >
+
+
+
+
+
+ {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 && (
+
+ )}
+
+
+ {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
+}