From de6df374a2a9b808cbbed57be3e5e0a874eabf68 Mon Sep 17 00:00:00 2001 From: suvarnakale Date: Mon, 17 Jun 2024 20:09:21 +0530 Subject: [PATCH 1/2] Issue #PS-874 feat: Implentation of Guide tour --- package.json | 1 + public/locales/en/common.json | 12 + public/locales/hi/common.json | 12 + public/locales/mr/common.json | 12 + src/components/GuideTour.tsx | 127 +++++ src/components/Header.tsx | 9 +- src/components/MonthCalender.tsx | 20 +- src/pages/dashboard.tsx | 844 ++++++++++++++++--------------- src/utils/tourGuideSteps.ts | 31 ++ 9 files changed, 665 insertions(+), 403 deletions(-) create mode 100644 src/components/GuideTour.tsx create mode 100644 src/utils/tourGuideSteps.ts diff --git a/package.json b/package.json index e0ab56d7..ef0a198b 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "react-circular-progressbar": "^2.1.0", "react-dom": "^18", "react-ga4": "^2.1.0", + "react-joyride": "^2.8.2", "react-toastify": "^10.0.5", "sharp": "^0.33.3" }, diff --git a/public/locales/en/common.json b/public/locales/en/common.json index f5dd1409..fa46b531 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -182,5 +182,17 @@ "MOBILISATION_METHOD": "How Was the Learner Mobilized?", "MONTH_MOBILISATION": "Month of Mobilisation", "DROP_OUT_REASON": "Reason for Drop Out From School" + }, + "GUIDE_TOUR": { + "STEP_1": "Welcome! let’s get you familiar with the dashboard", + "STEP_2": "Use the calendar to view day-wise attendance of learners", + "STEP_3": "Check daily attendance percentage for each Center", + "STEP_4": "Mark each day’s attendance of learners", + "STEP_5": "Check Center attendance over the last 7 days", + "STEP_6": "View learners with low attendance in the past 7 days", + "PREVIOUS": "Previous", + "NEXT": "Next", + "SKIP": "Skip", + "FINISH": "Finish" } } diff --git a/public/locales/hi/common.json b/public/locales/hi/common.json index 1053d174..93d723d1 100644 --- a/public/locales/hi/common.json +++ b/public/locales/hi/common.json @@ -181,5 +181,17 @@ "MOBILISATION_METHOD": "शिक्षार्थी को कैसे जोड़ा गया?", "MONTH_MOBILISATION": "जोड़ने का महीना", "DROP_OUT_REASON": "स्कूल छोड़ने का कारण" + }, + "GUIDE_TOUR": { + "STEP_1": "स्वागत है! आइए आपको डैशबोर्ड से परिचित कराते हैं", + "STEP_2": "शिक्षार्थियों की दिन-वार उपस्थिति देखने के लिए कैलेंडर का उपयोग करें", + "STEP_3": "प्रत्येक केंद्र के लिए दैनिक उपस्थिति प्रतिशत की जाँच करें", + "STEP_4": "शिक्षार्थियों की प्रत्येक दिन की उपस्थिति को चिह्नित करें", + "STEP_5": "पिछले 7 दिनों में केंद्र की उपस्थिति की जाँच करें", + "STEP_6": "पिछले 7 दिनों में कम उपस्थिति वाले शिक्षार्थियों को देखें", + "PREVIOUS": "पिछला", + "NEXT": "अगला", + "SKIP": "छोड़ें", + "FINISH": "समाप्त" } } diff --git a/public/locales/mr/common.json b/public/locales/mr/common.json index b97ca58e..a1c27336 100644 --- a/public/locales/mr/common.json +++ b/public/locales/mr/common.json @@ -181,5 +181,17 @@ "MOBILISATION_METHOD": "शिकणाऱ्याला कसे एकत्रित केले गेले?", "MONTH_MOBILISATION": "एकत्रीकरणाचा महिना", "DROP_OUT_REASON": "शाळा सोडण्याचे कारण" + }, + "GUIDE_TOUR": { + "STEP_1": "स्वागत आहे! चला तुम्हाला डॅशबोर्डशी परिचित करून देतो", + "STEP_2": "शिक्षार्थ्यांची दिवसवार उपस्थिती पाहण्यासाठी कॅलेंडर वापरा", + "STEP_3": "प्रत्येक केंद्रासाठी दैनंदिन उपस्थिती टक्केवारी तपासा", + "STEP_4": "शिक्षार्थ्यांची प्रत्येक दिवसाची उपस्थिती चिन्हांकित करा", + "STEP_5": "शेवटच्या 7 दिवसांत केंद्राची उपस्थिती तपासा", + "STEP_6": "शेवटच्या 7 दिवसांत कमी उपस्थिती असलेले शिक्षार्थी पहा", + "PREVIOUS": "मागील", + "NEXT": "पुढे", + "SKIP": "वगळा", + "FINISH": "पूर्ण" } } diff --git a/src/components/GuideTour.tsx b/src/components/GuideTour.tsx new file mode 100644 index 00000000..f8533b59 --- /dev/null +++ b/src/components/GuideTour.tsx @@ -0,0 +1,127 @@ +import React, { useEffect, useState } from 'react'; +import Joyride from 'react-joyride'; +import { getSteps } from '../utils/tourGuideSteps'; +import { useTheme } from '@mui/material/styles'; +import { useTranslation } from 'next-i18next'; +import { logEvent } from '@/utils/googleAnalytics'; + +interface JoyrideCallbackData { + action: string; + index: number; + type: string; + status: string; +} + +const GuideTour = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const steps = getSteps(t); + const totalSteps = steps.length; + const [runTour, setRunTour] = useState(true); + const [stepIndex, setStepIndex] = useState(1); + + useEffect(() => { + setRunTour(true); + setStepIndex(stepIndex); + const hasSeenTutorial = localStorage.getItem('hasSeenTutorial'); + + if (!hasSeenTutorial) { + setRunTour(true); + } else { + setRunTour(false); + } + }, []); + + const handleTourEnd = () => { + if (typeof window !== 'undefined' && window.localStorage) { + localStorage.setItem('hasSeenTutorial', 'true'); + } + }; + + const handleJoyrideCallback = (data: JoyrideCallbackData) => { + const { action, index, type, status } = data; + + if (status === 'finished' || status === 'skipped') { + setRunTour(false); + handleTourEnd(); + logEvent({ + action: 'skip-guide-tour/finished-guide-tour', + category: 'Dashboard Page', + label: 'Skip/ Finish Button Click', + }); + } else if (type === 'step:after' || type === 'tour:start') { + if (action === 'next') { + setStepIndex((prevIndex) => prevIndex + 1); + logEvent({ + action: 'next-button-clicked', + category: 'Dashboard Page', + label: 'Next Button Click', + }); + } else if (action === 'prev') { + setStepIndex((prevIndex) => prevIndex - 1); + logEvent({ + action: 'previous-button-clicked', + category: 'Dashboard Page', + label: 'Previous Button Click', + }); + } + } + }; + + return ( + <> + {runTour && ( + + )} + + ); +}; + +export default GuideTour; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index a16e94b0..e588386f 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -113,6 +113,13 @@ const Header: React.FC = () => { }, []); const [language, setLanguage] = React.useState(selectedLanguage); + let hasSeenTutorial = false; + if (typeof window !== 'undefined' && window.localStorage) { + const storedValue = localStorage.getItem('hasSeenTutorial'); + if (storedValue !== null) { + hasSeenTutorial = storedValue === 'true'; // Convert string 'true' or 'false' to boolean + } + } return ( @@ -120,7 +127,7 @@ const Header: React.FC = () => { sx={{ display: 'flex', justifyContent: 'center', - position: 'fixed', + position: hasSeenTutorial ? 'fixed' : 'relative', top: '0px', zIndex: '999', width: '100%', diff --git a/src/components/MonthCalender.tsx b/src/components/MonthCalender.tsx index 97dd9c8c..0bc4c858 100644 --- a/src/components/MonthCalender.tsx +++ b/src/components/MonthCalender.tsx @@ -87,7 +87,9 @@ const MonthCalender: React.FC = ({ if (moveDate && selectedDates) { const [startDate] = selectedDates; setSelectedDates([startDate, moveDate]); - onDateChange([startDate, moveDate]); + if (startDate !== null) { + onDateChange([startDate, moveDate]); + } } } }; @@ -100,7 +102,9 @@ const MonthCalender: React.FC = ({ if (endDate && selectedDates) { const [startDate] = selectedDates; setSelectedDates([startDate, endDate]); - onDateChange([startDate, endDate]); + if (startDate !== null) { + onDateChange([startDate, endDate]); + } } } }; @@ -252,8 +256,16 @@ const MonthCalender: React.FC = ({ return `${year}-${month}-${day}`; }; setDate(newDate); - setSelectedDates(Array.isArray(newDate) ? newDate : [newDate, newDate]); - onDateChange(newDate); + if (newDate !== undefined) { + let datesToSet: [Date | null, Date | null]; + if (Array.isArray(newDate)) { + datesToSet = [newDate[0] || null, newDate[1] || null]; + } else { + datesToSet = [newDate || null, newDate || null]; + } + setSelectedDates(datesToSet); + } + onDateChange(newDate as Date | Date[] | null); if (newDate === null) { setDate(null); diff --git a/src/pages/dashboard.tsx b/src/pages/dashboard.tsx index 0d45daf6..81d5fd45 100644 --- a/src/pages/dashboard.tsx +++ b/src/pages/dashboard.tsx @@ -56,7 +56,7 @@ import Loader from '../components/Loader'; import useDeterminePathColor from '../hooks/useDeterminePathColor'; import { cohortList } from '../services/CohortServices'; import { lowLearnerAttendanceLimit } from './../../app.config'; - +import GuideTour from '@/components/GuideTour'; interface DashboardProps { // buttonText: string; } @@ -86,7 +86,7 @@ const Dashboard: React.FC = () => { const [dateRange, setDateRange] = React.useState(''); const [allCenterAttendanceData, setAllCenterAttendanceData] = React.useState(cohortsData); - + const [isClient, setIsClient] = React.useState(false); const router = useRouter(); const theme = useTheme(); const determinePathColor = useDeterminePathColor(); @@ -96,6 +96,7 @@ const Dashboard: React.FC = () => { const formattedSevenDaysAgo = shortDateFormat(sevenDaysAgo); useEffect(() => { + setIsClient(true); const calculateDateRange = () => { const endRangeDate = new Date(); endRangeDate.setHours(23, 59, 59, 999); @@ -500,435 +501,479 @@ const Dashboard: React.FC = () => { label: 'More Details Link Clicked', }); }; - + let hasSeenTutorial = false; + if (typeof window !== 'undefined' && window.localStorage) { + const storedValue = localStorage.getItem('hasSeenTutorial'); + if (storedValue !== null) { + hasSeenTutorial = storedValue === 'true'; // Convert string 'true' or 'false' to boolean + } + } return ( <> - {!isAuthenticated && ( - - )} + {isClient && ( + <> + + <> + {!isAuthenticated && ( + + )} - {isAuthenticated && ( - -
- - - - {t('DASHBOARD.DASHBOARD')} - - - - {loading && ( - - )} - - - - - - {t('DASHBOARD.DAY_WISE_ATTENDANCE')} - + {isAuthenticated && ( + +
+ - {getMonthName(selectedDate)} + {t('DASHBOARD.DASHBOARD')} - {/* */} - logo - - - {cohortsData?.length > 1 ? ( - - - {cohort.name} - - )) + {cohortsData?.length !== 0 ? ( + manipulatedCohortData?.map((cohort) => ( + + {cohort.name} + + )) + ) : ( + + {t('COMMON.NO_DATA_FOUND')} + + )} + + ) : ( - - {t('COMMON.NO_DATA_FOUND')} + + {cohortsData[0]?.name} )} - - - ) : ( - - {cohortsData[0]?.name} - - )} - - - {/* TODO: Write logic to disable this block on all select */} - - - - - - - - {currentAttendance !== 'notMarked' && - currentAttendance !== 'futureDate' && ( - <> - - - - {t('DASHBOARD.PERCENT_ATTENDANCE', { - percent_students: - currentAttendance?.present_percentage, - })} - + + + {/* TODO: Write logic to disable this block on all select */} + + + + + + + + {currentAttendance !== 'notMarked' && + currentAttendance !== 'futureDate' && ( + <> + + + + {t('DASHBOARD.PERCENT_ATTENDANCE', { + percent_students: + currentAttendance?.present_percentage, + })} + + + {t('DASHBOARD.PRESENT_STUDENTS', { + present_students: + currentAttendance?.present_students, + total_students: + currentAttendance?.totalcount, + })} + + + + )} + {currentAttendance === 'notMarked' && + currentAttendance !== 'futureDate' && ( + + {t('DASHBOARD.NOT_MARKED')} + + )} + {currentAttendance === 'futureDate' && ( - {t('DASHBOARD.PRESENT_STUDENTS', { - present_students: - currentAttendance?.present_students, - total_students: - currentAttendance?.totalcount, - })} + {t('DASHBOARD.FUTURE_DATE_CANT_MARK')} - - - )} - {currentAttendance === 'notMarked' && - currentAttendance !== 'futureDate' && ( - + + + + {open && ( + { + if (isModified) { + showToastMessage( + t( + 'ATTENDANCE.ATTENDANCE_MODIFIED_SUCCESSFULLY' + ), + 'success' + ); + } else { + showToastMessage( + t( + 'ATTENDANCE.ATTENDANCE_MARKED_SUCCESSFULLY' + ), + 'success' + ); + } + setHandleSaveHasRun(!handleSaveHasRun); + }} + /> )} - - - - {open && ( - { - if (isModified) { - showToastMessage( - t('ATTENDANCE.ATTENDANCE_MODIFIED_SUCCESSFULLY'), - 'success' - ); - } else { - showToastMessage( - t('ATTENDANCE.ATTENDANCE_MARKED_SUCCESSFULLY'), - 'success' - ); - } - setHandleSaveHasRun(!handleSaveHasRun); - }} - /> - )} - - - - - + + + + - {/* Overview Card Section */} - - - + {/* Overview Card Section */} - - {t('DASHBOARD.OVERVIEW')} - - - - {t('DASHBOARD.MORE_DETAILS')} - - - + + + + {t('DASHBOARD.OVERVIEW')} + + + + {t('DASHBOARD.MORE_DETAILS')} + + + + + + {t('DASHBOARD.LAST_SEVEN_DAYS_RANGE', { + date_range: dateRange, + })} + + + + {loading && ( + + )} - - {t('DASHBOARD.LAST_SEVEN_DAYS_RANGE', { - date_range: dateRange, - })} - - - - {loading && ( - - )} - - - {classId && - classId !== 'all' && - cohortsData && - lowAttendanceLearnerList ? ( - - - - - - - ))} - valuePartOne={ - Array.isArray(lowAttendanceLearnerList) && - lowAttendanceLearnerList.length > 2 - ? `${lowAttendanceLearnerList[0]}, ${lowAttendanceLearnerList[1]}` - : lowAttendanceLearnerList.length === 2 - ? `${lowAttendanceLearnerList[0]}, ${lowAttendanceLearnerList[1]}` - : lowAttendanceLearnerList.length === 1 - ? `${lowAttendanceLearnerList[0]}` - : Array.isArray(lowAttendanceLearnerList) && - lowAttendanceLearnerList.length === 0 - ? t( - 'ATTENDANCE.NO_LEARNER_WITH_LOW_ATTENDANCE' - ) - : t('ATTENDANCE.N/A') - } - valuePartTwo={ - Array.isArray(lowAttendanceLearnerList) && - lowAttendanceLearnerList.length > 2 - ? `${t('COMMON.AND')} ${lowAttendanceLearnerList.length - 2} ${t('COMMON.MORE')}` - : null - } - /> - - - ) : ( - - {allCenterAttendanceData.map( - (item: { - cohortId: React.Key | null | undefined; - name: string; - presentPercentage: number; - }) => ( - - + + {classId && + classId !== 'all' && + cohortsData && + lowAttendanceLearnerList ? ( + + + + + + + ))} + valuePartOne={ + Array.isArray(lowAttendanceLearnerList) && + lowAttendanceLearnerList.length > 2 + ? `${lowAttendanceLearnerList[0]}, ${lowAttendanceLearnerList[1]}` + : lowAttendanceLearnerList.length === 2 + ? `${lowAttendanceLearnerList[0]}, ${lowAttendanceLearnerList[1]}` + : lowAttendanceLearnerList.length === 1 + ? `${lowAttendanceLearnerList[0]}` + : Array.isArray( + lowAttendanceLearnerList + ) && + lowAttendanceLearnerList.length === 0 + ? t( + 'ATTENDANCE.NO_LEARNER_WITH_LOW_ATTENDANCE' + ) + : t('ATTENDANCE.N/A') + } + valuePartTwo={ + Array.isArray(lowAttendanceLearnerList) && + lowAttendanceLearnerList.length > 2 + ? `${t('COMMON.AND')} ${lowAttendanceLearnerList.length - 2} ${t('COMMON.MORE')}` + : null + } + /> + - ) - )} - - )} - - - + ) : ( + + {allCenterAttendanceData.map( + (item: { + cohortId: React.Key | null | undefined; + name: string; + presentPercentage: number; + }) => ( + + + + ) + )} + + )} + + + - {/* + {/* = () => { /> */} - + + )} + + )} ); diff --git a/src/utils/tourGuideSteps.ts b/src/utils/tourGuideSteps.ts new file mode 100644 index 00000000..84183ed8 --- /dev/null +++ b/src/utils/tourGuideSteps.ts @@ -0,0 +1,31 @@ +import { TFunction } from 'next-i18next'; +export const getSteps = (t: TFunction) => [ + { + target: 'joyride-step-0', + content: t('GUIDE_TOUR.STEP_0'), + }, + { + target: '.joyride-step-1', + content: t('GUIDE_TOUR.STEP_1'), + }, + { + target: '.joyride-step-2', + content: t('GUIDE_TOUR.STEP_2'), + }, + { + target: '.joyride-step-3', + content: t('GUIDE_TOUR.STEP_3'), + }, + { + target: '.joyride-step-4', + content: t('GUIDE_TOUR.STEP_4'), + }, + { + target: '.joyride-step-5', + content: t('GUIDE_TOUR.STEP_5'), + }, + { + target: '.joyride-step-6', + content: t('GUIDE_TOUR.STEP_6'), + }, +]; From 1a9d992c879b20148b472f1ca8354e4d4924e5fa Mon Sep 17 00:00:00 2001 From: suvarnakale Date: Mon, 17 Jun 2024 20:48:44 +0530 Subject: [PATCH 2/2] Issue #PS-874 chore: PR updated --- src/components/GuideTour.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/GuideTour.tsx b/src/components/GuideTour.tsx index f8533b59..837dbcd5 100644 --- a/src/components/GuideTour.tsx +++ b/src/components/GuideTour.tsx @@ -25,11 +25,7 @@ const GuideTour = () => { setStepIndex(stepIndex); const hasSeenTutorial = localStorage.getItem('hasSeenTutorial'); - if (!hasSeenTutorial) { - setRunTour(true); - } else { - setRunTour(false); - } + setRunTour(!hasSeenTutorial); }, []); const handleTourEnd = () => {