diff --git a/src/components/CourseProfileStickyHeader/index.tsx b/src/components/CourseProfileStickyHeader/index.tsx deleted file mode 100644 index 57915ccb0..000000000 --- a/src/components/CourseProfileStickyHeader/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { View, Text } from 'react-native'; -import ProgressBar from '../cards/ProgressBar'; -import styles from './styles'; - -interface CourseProfileStickyHeaderProps { - progress: number, - title: string -} - -const CourseProfileStickyHeader = ({ progress, title }: CourseProfileStickyHeaderProps) => ( - - {title} - - {(progress * 100).toFixed(0)}% - - - - - -); - -export default CourseProfileStickyHeader; diff --git a/src/components/CourseProfileStickyHeader/styles.ts b/src/components/CourseProfileStickyHeader/styles.ts deleted file mode 100644 index 738501566..000000000 --- a/src/components/CourseProfileStickyHeader/styles.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { StyleSheet } from 'react-native'; -import { WHITE, GREY } from '../../styles/colors'; -import { BORDER_WIDTH, MARGIN, PADDING, SCREEN_WIDTH } from '../../styles/metrics'; -import { FIRA_SANS_REGULAR, FIRA_SANS_BOLD } from '../../styles/fonts'; - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - backgroundColor: WHITE, - paddingVertical: PADDING.LG, - paddingHorizontal: PADDING.XL, - alignItems: 'center', - borderBottomWidth: 2 * BORDER_WIDTH, - borderColor: GREY[200], - }, - title: { - ...FIRA_SANS_BOLD.MD, - color: GREY[800], - maxWidth: (2 * SCREEN_WIDTH) / 3, - }, - progressBarContainer: { - flex: 1, - alignItems: 'flex-end', - }, - progressPercentage: { - ...FIRA_SANS_REGULAR.SM, - color: GREY[600], - marginVertical: MARGIN.XS, - }, - progressBar: { - width: 64, - }, -}); - -export default styles; diff --git a/src/components/ELearningCell/index.tsx b/src/components/ELearningCell/index.tsx index 9dc515bc0..ca6de1fc3 100644 --- a/src/components/ELearningCell/index.tsx +++ b/src/components/ELearningCell/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { View, TouchableOpacity } from 'react-native'; import { ELearningStepType } from '../../types/StepTypes'; import { CourseModeType } from '../../types/CourseTypes'; @@ -18,7 +18,7 @@ type ELearningCellProps = { endedActivity?: string, } -const ELearningCell = ({ step, index, profileId, mode, endedActivity = '' }: ELearningCellProps) => { +const ELearningCell = React.memo(({ step, index, profileId, mode, endedActivity = '' }: ELearningCellProps) => { const [isOpen, setIsOpen] = useState(false); const onPressChevron = () => { setIsOpen(prevState => !prevState); }; const [iconButtonStyle, setIconButtonStyle] = useState(styles.iconButtonContainer); @@ -48,6 +48,6 @@ const ELearningCell = ({ step, index, profileId, mode, endedActivity = '' }: ELe {isOpen && } ); -}; +}); export default ELearningCell; diff --git a/src/components/Modal/styles.ts b/src/components/Modal/styles.ts index b39096c4f..a1e11b8bc 100644 --- a/src/components/Modal/styles.ts +++ b/src/components/Modal/styles.ts @@ -15,6 +15,7 @@ export default StyleSheet.create({ backgroundColor: WHITE, borderRadius: BORDER_RADIUS.MD, width: '90%', + maxHeight: '60%', padding: PADDING.LG, }, }); diff --git a/src/components/activities/ActivityCell/index.tsx b/src/components/activities/ActivityCell/index.tsx index 0d3faaa47..7b9e6870f 100644 --- a/src/components/activities/ActivityCell/index.tsx +++ b/src/components/activities/ActivityCell/index.tsx @@ -1,4 +1,4 @@ -import { useReducer, useEffect } from 'react'; +import React, { useReducer, useEffect } from 'react'; import { Text, View, TouchableOpacity } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useNavigation } from '@react-navigation/native'; @@ -32,7 +32,7 @@ const colorsReducer = (state: ColorStateType, action: ColorActionType): ColorSta } }; -const ActivityCell = ({ activity, profileId, mode }: ActivityCellProps) => { +const ActivityCell = React.memo(({ activity, profileId, mode }: ActivityCellProps) => { const setQuestionnaireAnswersList = useSetQuestionnaireAnswersList(); const disabled = !activity.cards.length; const isCompleted = !!activity.activityHistories?.length; @@ -83,6 +83,6 @@ const ActivityCell = ({ activity, profileId, mode }: ActivityCellProps) => { ); -}; +}); export default ActivityCell; diff --git a/src/components/activities/ActivityList/index.tsx b/src/components/activities/ActivityList/index.tsx index 076e31325..3b08cc8e6 100644 --- a/src/components/activities/ActivityList/index.tsx +++ b/src/components/activities/ActivityList/index.tsx @@ -20,7 +20,8 @@ const ActivityList = ({ activities, profileId, mode }: ActivityListProps) => { return ( item._id} renderItem={({ item }) => renderActivityCell(item)} ItemSeparatorComponent={renderSeparator} - contentContainerStyle={styles.cell} showsHorizontalScrollIndicator={false} /> + contentContainerStyle={styles.cell} showsHorizontalScrollIndicator={false} initialNumToRender={5} + maxToRenderPerBatch={10} windowSize={5} /> ); }; diff --git a/src/components/steps/LiveCell/index.tsx b/src/components/steps/LiveCell/index.tsx index f0f7bbf3a..6d32eedbc 100644 --- a/src/components/steps/LiveCell/index.tsx +++ b/src/components/steps/LiveCell/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { View, TouchableOpacity } from 'react-native'; import { Feather } from '@expo/vector-icons'; import get from 'lodash/get'; @@ -19,7 +19,7 @@ type LiveCellProps = { slots?: SlotType[], } -const LiveCell = ({ step, index, mode, slots = [] }: LiveCellProps) => { +const LiveCell = React.memo(({ step, index, mode, slots = [] }: LiveCellProps) => { const [isModalVisible, setIsModalVisible] = useState(false); const [stepSlots, setStepSlots] = useState([]); const [dates, setDates] = useState([]); @@ -41,8 +41,8 @@ const LiveCell = ({ step, index, mode, slots = [] }: LiveCellProps) => { return ( <> - + {isModalVisible && ()} @@ -52,6 +52,6 @@ const LiveCell = ({ step, index, mode, slots = [] }: LiveCellProps) => { ); -}; +}); export default LiveCell; diff --git a/src/core/data/constants.ts b/src/core/data/constants.ts index e10500c01..f90d5794e 100644 --- a/src/core/data/constants.ts +++ b/src/core/data/constants.ts @@ -121,6 +121,7 @@ export const DAY_OF_MONTH = 'd'; export const DAY_OF_WEEK_SHORT = 'ccc'; export const YEAR = 'yyyy'; export const DAY_D_MONTH = 'cccc d LLLL'; +export const DAY_D_MONTH_YEAR = 'ccc d LLL yyyy'; // COMPANIDURATION FORMATS export const LONG_DURATION_H_MM = 'h\'h\' mm\'min\''; diff --git a/src/screens/courses/list/TrainerCourses/index.tsx b/src/screens/courses/list/TrainerCourses/index.tsx index ceaf643d1..0d2df516d 100644 --- a/src/screens/courses/list/TrainerCourses/index.tsx +++ b/src/screens/courses/list/TrainerCourses/index.tsx @@ -1,6 +1,6 @@ import 'array-flat-polyfill'; import { useState, useEffect, useCallback, useMemo } from 'react'; -import { Text, View, ScrollView, ImageBackground } from 'react-native'; +import { Text, View, ImageBackground, FlatList } from 'react-native'; import { useIsFocused, CompositeScreenProps } from '@react-navigation/native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { StackScreenProps } from '@react-navigation/stack'; @@ -119,28 +119,33 @@ const TrainerCourses = ({ navigation }: TrainerCoursesProps) => { : [] ), [coursesDisplays]); + const renderHeader = () => <> + Espace intervenant + {!!nextSteps.length && + + + } + ; + + const renderFooter = () => (!!coursesDisplays.length && + + ); + + const renderCourseDisplay = (content: CourseDisplayType) => + + + ; + return ( - - Espace intervenant - {!!nextSteps.length && - - - - } - {coursesDisplays.length - ? coursesDisplays.map(content => ( - - - - )) - : - } - - + + item.title} ListHeaderComponent={renderHeader} + renderItem={({ item }) => renderCourseDisplay(item)} showsVerticalScrollIndicator={false} + ListEmptyComponent={} ListFooterComponent={renderFooter}/> + ); }; diff --git a/src/screens/courses/profile/AdminCourseProfile/index.tsx b/src/screens/courses/profile/AdminCourseProfile/index.tsx index 49059324b..a6050e685 100644 --- a/src/screens/courses/profile/AdminCourseProfile/index.tsx +++ b/src/screens/courses/profile/AdminCourseProfile/index.tsx @@ -1,5 +1,14 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; -import { View, BackHandler, Text, ScrollView, TouchableOpacity, Image, FlatList } from 'react-native'; +import { + View, + BackHandler, + Text, + ScrollView, + TouchableOpacity, + Image, + FlatList, + ActivityIndicator, +} from 'react-native'; import { Feather } from '@expo/vector-icons'; import pick from 'lodash/pick'; import uniqBy from 'lodash/uniqBy'; @@ -160,13 +169,12 @@ const AdminCourseProfile = ({ route, navigation }: AdminCourseProfileProps) => { const getCourse = async () => { try { const fetchedCourse = await Courses.getCourse(route.params.courseId, OPERATIONS) as BlendedCourseType; - await refreshAttendanceSheets(fetchedCourse._id); - await getQuestionnaireQRCode(fetchedCourse._id); + await Promise.all([refreshAttendanceSheets(fetchedCourse._id), getQuestionnaireQRCode(fetchedCourse._id)]); if (fetchedCourse.slots.length) setFirstSlot(fetchedCourse.slots[0]); - setCourse(fetchedCourse as BlendedCourseType); setTitle(getTitle(fetchedCourse)); setIsSingle(SINGLE_COURSES_SUBPROGRAM_IDS.includes(fetchedCourse.subProgram._id)); + setCourse(fetchedCourse as BlendedCourseType); } catch (e: any) { console.error(e); setCourse(null); @@ -272,7 +280,7 @@ const AdminCourseProfile = ({ route, navigation }: AdminCourseProfileProps) => { const goToAttendanceSheetUpload = () => navigation.navigate('CreateAttendanceSheet', { isSingle }); - return course && has(course, 'subProgram.program') && ( + return course && has(course, 'subProgram.program') ? ( @@ -338,7 +346,10 @@ const AdminCourseProfile = ({ route, navigation }: AdminCourseProfileProps) => { {imagePreview.visible && } - ); + ) + : + + ; }; export default AdminCourseProfile; diff --git a/src/screens/courses/profile/LearnerCourseProfile/index.tsx b/src/screens/courses/profile/LearnerCourseProfile/index.tsx index 9de5049f1..08e4e125c 100644 --- a/src/screens/courses/profile/LearnerCourseProfile/index.tsx +++ b/src/screens/courses/profile/LearnerCourseProfile/index.tsx @@ -4,14 +4,11 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { View, Text, - ScrollView, BackHandler, TouchableOpacity, ImageSourcePropType, ActivityIndicator, - LayoutChangeEvent, - NativeSyntheticEvent, - NativeScrollEvent, + FlatList, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useIsFocused, CompositeScreenProps } from '@react-navigation/native'; @@ -27,13 +24,12 @@ import { RootStackParamList, RootBottomTabParamList } from '../../../../types/Na import Courses from '../../../../api/courses'; import Questionnaires from '../../../../api/questionnaires'; import { WHITE, GREY } from '../../../../styles/colors'; -import { ICON, SCROLL_EVENT_THROTTLE } from '../../../../styles/metrics'; +import { ICON } from '../../../../styles/metrics'; import commonStyles from '../../../../styles/common'; import { CourseType, BlendedCourseType, ELearningProgramType } from '../../../../types/CourseTypes'; import styles from '../styles'; import { useGetLoggedUserId, useSetStatusBarVisible } from '../../../../store/main/hooks'; import ProgressBar from '../../../../components/cards/ProgressBar'; -import CourseProfileStickyHeader from '../../../../components/CourseProfileStickyHeader'; import NiSecondaryButton from '../../../../components/form/SecondaryButton'; import PendingActionsContainer from '../../../../components/learnerPendingActions/PendingActionsContainer'; import { QuestionnaireType } from '../../../../types/QuestionnaireType'; @@ -56,8 +52,6 @@ const LearnerCourseProfile = ({ route, navigation }: LearnerCourseProfileProps) const [questionnaires, setQuestionnaires] = useState([]); const [source, setSource] = useState(require('../../../../../assets/images/authentication_background_image.webp')); - const [isHeaderSticky, setIsHeaderSticky] = useState (false); - const [progressBarY, setProgressBarY] = useState (0); const [isLoading, setIsLoading] = useState(false); const attendanceSheetsToSign = useMemo(() => @@ -152,31 +146,6 @@ const LearnerCourseProfile = ({ route, navigation }: LearnerCourseProfileProps) setIsLoading(false); }; - const isProgressBarOnTop = (event: NativeSyntheticEvent) => { - const { y } = event.nativeEvent.contentOffset; - setIsHeaderSticky(y >= progressBarY); - }; - - const getProgressBarY = (event: LayoutChangeEvent) => { - const { layout } = event.nativeEvent; - setProgressBarY(layout.y); - }; - - const getHeader = () => course && has(course, 'subProgram.program') && ( - - {isHeaderSticky - ? - : - ÉTAPES - - - - {(getCourseProgress(course) * 100).toFixed(0)}% - - } - - ); - const goToAbout = () => { if (!course) return; if (course.subProgram.isStrictlyELearning) { @@ -191,34 +160,43 @@ const LearnerCourseProfile = ({ route, navigation }: LearnerCourseProfileProps) } }; - return course && has(course, 'subProgram.program') ? ( - - - - - - - {!!(questionnaires.length || attendanceSheetsToSign.length) && + const renderHeader = () => course && has(course, 'subProgram.program') && <> + + + + + {!!(questionnaires.length || attendanceSheetsToSign.length) && - } - {getHeader()} - {renderStepList(course, LEARNER, route)} - {course.areLastSlotAttendancesValidated && - - {isLoading - ? - : - - Attestation - } - + } + + ÉTAPES + + + + {(getCourseProgress(course) * 100).toFixed(0)}% + + ; + + const renderFooter = () => + {course?.areLastSlotAttendancesValidated && + + {isLoading + ? + : + + Attestation } - + } + ; + + return course && has(course, 'subProgram.program') ? ( + + item._id} ListHeaderComponent={renderHeader} + renderItem={({ item, index }) => renderStepList(course, LEARNER, route, item, index)} + showsVerticalScrollIndicator={IS_WEB} ListFooterComponent={renderFooter} /> ) : diff --git a/src/screens/courses/profile/SubProgramProfile/index.tsx b/src/screens/courses/profile/SubProgramProfile/index.tsx index bf3976840..fb18b6825 100644 --- a/src/screens/courses/profile/SubProgramProfile/index.tsx +++ b/src/screens/courses/profile/SubProgramProfile/index.tsx @@ -3,11 +3,10 @@ import { View, Text, ImageBackground, - ScrollView, - StyleProp, - ViewStyle, BackHandler, ImageSourcePropType, + FlatList, + ActivityIndicator, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useIsFocused, CompositeScreenProps } from '@react-navigation/native'; @@ -15,7 +14,7 @@ import { StackScreenProps } from '@react-navigation/stack'; import get from 'lodash/get'; import { LinearGradient } from 'expo-linear-gradient'; import SubPrograms from '../../../../api/subPrograms'; -import { WHITE } from '../../../../styles/colors'; +import { GREY, WHITE } from '../../../../styles/colors'; import { ICON } from '../../../../styles/metrics'; import FeatherButton from '../../../../components/icons/FeatherButton'; import { TESTER } from '../../../../core/data/constants'; @@ -83,22 +82,25 @@ const SubProgramProfile = ({ route, navigation }: SubProgramProfileProps) => { return () => { BackHandler.removeEventListener('hardwareBackPress', hardwareBackPress); }; }, [hardwareBackPress]); - return subProgram && subProgram.steps && ( + const renderHeader = () => + + + + {programName} + + ; + + return subProgram && subProgram.steps ? ( - - }> - - - - {programName} - - - {renderStepList({ subProgram }, TESTER, route)} - + item._id} ListHeaderComponent={renderHeader} + renderItem={({ item, index }) => renderStepList(subProgram, TESTER, route, item, index)} + showsVerticalScrollIndicator={false} /> - ); + ) + : + + ; }; export default SubProgramProfile; diff --git a/src/screens/courses/profile/TrainerCourseProfile/index.tsx b/src/screens/courses/profile/TrainerCourseProfile/index.tsx index 60b6b7203..cc3fb5494 100644 --- a/src/screens/courses/profile/TrainerCourseProfile/index.tsx +++ b/src/screens/courses/profile/TrainerCourseProfile/index.tsx @@ -1,7 +1,7 @@ // @ts-nocheck import { useState, useEffect, useCallback } from 'react'; -import { View, ScrollView, BackHandler, ImageSourcePropType } from 'react-native'; +import { View, BackHandler, ImageSourcePropType, FlatList, ActivityIndicator } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useIsFocused, CompositeScreenProps } from '@react-navigation/native'; import get from 'lodash/get'; @@ -45,10 +45,10 @@ const TrainerCourseProfile = ({ const getCourse = async () => { try { const fetchedCourse = await Courses.getCourse(route.params.courseId, PEDAGOGY); + const programImage = get(fetchedCourse, 'subProgram.program.image.link') || ''; + setCourse(fetchedCourse); setTitle(getTitle(fetchedCourse)); - - const programImage = get(fetchedCourse, 'subProgram.program.image.link') || ''; if (programImage) setSource({ uri: programImage }); } catch (e: any) { console.error(e); @@ -84,20 +84,26 @@ const TrainerCourseProfile = ({ else navigation.navigate(screen, { courseId: course._id }); }; - return course && has(course, 'subProgram.program') && ( + const renderHeader = () => <> + + + goTo(ADMIN_SCREEN)} icon='folder' color={GREY[700]} + customStyle={styles.adminButton} borderColor={GREY[200]} bgColor={GREY[200]} font={FIRA_SANS_MEDIUM.LG} /> + goTo(ABOUT_SCREEN)} icon='info' borderColor={GREY[200]} + bgColor={WHITE} font={FIRA_SANS_MEDIUM.LG} /> + + ; + + return course && has(course, 'subProgram.program') ? ( - - - - goTo(ADMIN_SCREEN)} icon='folder' color={GREY[700]} - customStyle={styles.adminButton} borderColor={GREY[200]} bgColor={GREY[200]} font={FIRA_SANS_MEDIUM.LG} /> - goTo(ABOUT_SCREEN)} icon='info' borderColor={GREY[200]} - bgColor={WHITE} font={FIRA_SANS_MEDIUM.LG} /> - - {renderStepList(course, TRAINER, route)} - + item._id} ListHeaderComponent={renderHeader} + renderItem={({ item, index }) => renderStepList(course, TRAINER, route, item, index)} + showsVerticalScrollIndicator={false} /> - ); + ) + : + + ; }; export default TrainerCourseProfile; diff --git a/src/screens/courses/profile/helper.tsx b/src/screens/courses/profile/helper.tsx index f5e78dd65..b8c6e0982 100644 --- a/src/screens/courses/profile/helper.tsx +++ b/src/screens/courses/profile/helper.tsx @@ -25,12 +25,12 @@ const renderStepCell = (item, index, course, mode, route) => { const renderSeparator = () => ; -export const renderStepList = (course, mode, route) => - {course.subProgram.steps.map((s, index) => - {index !== 0 && renderSeparator()} - {renderStepCell(s, index, course, mode, route)} - )} -; +export const renderStepList = (course, mode, route, item, index) => ( + <> + {renderSeparator()} + {renderStepCell(item, index, course, mode, route)} + +); export const getTitle = (course) => { if (!course) return ''; diff --git a/src/screens/explore/BlendedAbout/index.tsx b/src/screens/explore/BlendedAbout/index.tsx index 469f53ba6..02e996f67 100644 --- a/src/screens/explore/BlendedAbout/index.tsx +++ b/src/screens/explore/BlendedAbout/index.tsx @@ -1,35 +1,19 @@ import { useEffect, useState, useMemo } from 'react'; -import { View, Text, Image, TouchableOpacity } from 'react-native'; +import { View, Text, Image, TouchableOpacity, FlatList } from 'react-native'; import Markdown from 'react-native-markdown-display'; import { StackScreenProps } from '@react-navigation/stack'; import { RootStackParamList } from '../../../types/NavigationType'; import CompaniDate from '../../../core/helpers/dates/companiDates'; -import { ascendingSort } from '../../../core/helpers/dates/utils'; import About from '../../../components/About'; import styles from './styles'; import { capitalize, formatIdentity } from '../../../core/helpers/utils'; import commonStyles, { markdownStyle } from '../../../styles/common'; import InternalRulesModal from '../../../components/InternalRulesModal'; import ContactInfoContainer from '../../../components/ContactInfoContainer'; -import { - LEARNER, - DAY_OF_WEEK_SHORT, - DAY_OF_MONTH, - MONTH_SHORT, - YEAR, - LONG_FIRSTNAME_LONG_LASTNAME, -} from '../../../core/data/constants'; +import { DAY_D_MONTH_YEAR, LEARNER, LONG_FIRSTNAME_LONG_LASTNAME } from '../../../core/data/constants'; interface BlendedAboutProps extends StackScreenProps {} -const formatDate = (date: Date) => { - const dayOfWeek = capitalize(CompaniDate(date).format(DAY_OF_WEEK_SHORT)); - const dayOfMonth = capitalize(CompaniDate(date).format(DAY_OF_MONTH)); - const month = capitalize(CompaniDate(date).format(MONTH_SHORT)); - const year = capitalize(CompaniDate(date).format(YEAR)); - return `${dayOfWeek} ${dayOfMonth} ${month} ${year}`; -}; - const BlendedAbout = ({ route, navigation }: BlendedAboutProps) => { const { course, mode } = route.params; const program = course.subProgram?.program || null; @@ -41,9 +25,7 @@ const BlendedAbout = ({ route, navigation }: BlendedAboutProps) => { const formattedDates = useMemo(() => { if (!course.slots.length) return []; - const formattedSlots = course.slots - .sort(ascendingSort('startDate')) - .map(slot => formatDate(slot.startDate)); + const formattedSlots = course.slots.map(slot => capitalize(CompaniDate(slot.startDate).format(DAY_D_MONTH_YEAR))); return [...new Set(formattedSlots)]; }, [course.slots]); @@ -65,9 +47,9 @@ const BlendedAbout = ({ route, navigation }: BlendedAboutProps) => { <> Dates de formation - {formattedDates.map((item, idx) => - {`- ${item}`} - )} + {`- ${item}`}} + /> } {!!course.trainer && <>