From 14aa15bdc52703898f46b58fbc9486de328f095a Mon Sep 17 00:00:00 2001 From: Romain Le Cellier Date: Fri, 14 Apr 2023 15:55:11 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20teacher=20dashboard=20cou?= =?UTF-8?q?rse=20loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add minimum loading time and transition when course apear --- .../DashboardCourseList/_styles.scss | 25 +++++++++++ .../components/DashboardCourseList/index.tsx | 44 +++++++++++++------ src/frontend/js/hooks/useIsLoading.ts | 43 ++++++++++++++++++ src/frontend/package.json | 2 + src/frontend/yarn.lock | 29 +++++++++++- 5 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 src/frontend/js/hooks/useIsLoading.ts diff --git a/src/frontend/js/components/DashboardCourseList/_styles.scss b/src/frontend/js/components/DashboardCourseList/_styles.scss index b14eb45853..d5a203a41f 100644 --- a/src/frontend/js/components/DashboardCourseList/_styles.scss +++ b/src/frontend/js/components/DashboardCourseList/_styles.scss @@ -13,9 +13,34 @@ $r-course-glimpse-dashboard-gutter: rem-calc(30px) !default; margin-bottom: 10px; } + // + // Loading transition + // + .fade-in-enter { + opacity: 0; + } + .fade-in-enter-active { + opacity: 1; + transition: opacity 300ms; + } + .fade-in-exit { + display: none; + opacity: 0; + } + .fade-in-exit-active { + display: none; + opacity: 0; + } + // // Course Glimpse in dashboards // + &__placeholder { + display: flex; + justify-content: center; + align-items: center; + height: rem-calc(360px); + } .dashboard { &__course-glimpse-list { diff --git a/src/frontend/js/components/DashboardCourseList/index.tsx b/src/frontend/js/components/DashboardCourseList/index.tsx index 96da6ef4bc..473c28f595 100644 --- a/src/frontend/js/components/DashboardCourseList/index.tsx +++ b/src/frontend/js/components/DashboardCourseList/index.tsx @@ -1,3 +1,5 @@ +import { useRef } from 'react'; +import { CSSTransition } from 'react-transition-group'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; import queryString from 'query-string'; @@ -7,6 +9,7 @@ import { useCourses, TeacherCourseSearchFilters } from 'hooks/useCourses'; import { getDashboardRoutePath } from 'widgets/Dashboard/utils/dashboardRoutes'; import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherRouteMessages'; import context from 'utils/context'; +import useIsLoading from 'hooks/useIsLoading'; const messages = defineMessages({ loading: { @@ -30,6 +33,10 @@ const DashboardCourseList = ({ titleTranslated, filters }: DashboardCourseListPr states: { fetching }, } = coursesResults; + const isLoading = useIsLoading([fetching], 300); + + const fadeInNodeRef = useRef(null); + return (
{titleTranslated && ( @@ -41,20 +48,31 @@ const DashboardCourseList = ({ titleTranslated, filters }: DashboardCourseListPr

{titleTranslated}

)} - {fetching && ( - - - - - - )} - {!fetching && courses.length && ( - + {isLoading && ( +
+ + + + + +
)} + + +
+ +
+
); }; diff --git a/src/frontend/js/hooks/useIsLoading.ts b/src/frontend/js/hooks/useIsLoading.ts new file mode 100644 index 0000000000..0a68096a1b --- /dev/null +++ b/src/frontend/js/hooks/useIsLoading.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react'; + +// await new Promise(resolve => setTimeout(resolve, 1000)) + +const useIsLoading = (loadingStates: boolean[], delayInMillisecond = 500) => { + const [isLoading, setIsLoading] = useState(!loadingStates.every((v) => v === false)); + const [timerId, setTimerId] = useState>(); + const [canStopLoading, setCanStopLoading] = useState(true); + + const startLoading = () => { + setCanStopLoading(false); + setIsLoading(true); + setTimerId( + setTimeout(() => { + setCanStopLoading(true); + }, delayInMillisecond), + ); + }; + + const stopLoading = () => { + setIsLoading(false); + clearTimeout(timerId); + setTimerId(undefined); + }; + + useEffect(() => { + const newIsLoading = !loadingStates.every((v) => v === false); + if (newIsLoading && !timerId) { + startLoading(); + } + }, [loadingStates, timerId]); + + useEffect(() => { + const newIsLoading = !loadingStates.every((v) => v === false); + if (isLoading && !newIsLoading && canStopLoading) { + stopLoading(); + } + }, [loadingStates, isLoading, canStopLoading]); + + return isLoading; +}; + +export default useIsLoading; diff --git a/src/frontend/package.json b/src/frontend/package.json index f614008750..e66dfeb14a 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -75,6 +75,7 @@ "@types/react-autosuggest": "10.1.6", "@types/react-dom": "18.0.11", "@types/react-modal": "3.13.1", + "@types/react-transition-group": "4.4.5", "@types/uuid": "9.0.1", "@typescript-eslint/eslint-plugin": "5.58.0", "@typescript-eslint/parser": "5.58.0", @@ -122,6 +123,7 @@ "react-intl": "6.3.2", "react-modal": "3.16.1", "react-router-dom": "6.10.0", + "react-transition-group": "4.4.5", "sass": "1.62.0", "source-map-loader": "4.0.1", "storybook": "7.0.3", diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index d20ae85c3d..b366695530 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -1076,7 +1076,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.17.8", "@babel/runtime@^7.20.7", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.17.8", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== @@ -3268,6 +3268,13 @@ dependencies: "@types/react" "*" +"@types/react-transition-group@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" + integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@16 || 17 || 18", "@types/react@18.0.34", "@types/react@>=16": version "18.0.34" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.34.tgz#e553444a578f023e6e1ac499514688fb80b0a984" @@ -5008,6 +5015,14 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-serializer@^1.0.1: version "1.4.1" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" @@ -8676,7 +8691,7 @@ prompts@^2.0.1, prompts@^2.4.0: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -8981,6 +8996,16 @@ react-themeable@^1.1.0: dependencies: object-assign "^3.0.0" +react-transition-group@4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"