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 1d8d3ffcb1..8757eb2ee3 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.57.0",
"@typescript-eslint/parser": "5.57.0",
@@ -121,6 +122,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.60.0",
"source-map-loader": "4.0.1",
"tsconfig-paths-webpack-plugin": "4.0.1",
diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock
index 16ac704db2..f7912706a6 100644
--- a/src/frontend/yarn.lock
+++ b/src/frontend/yarn.lock
@@ -1256,7 +1256,7 @@
dependencies:
regenerator-runtime "^0.13.2"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.17.8", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.0", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.17.8", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.0", "@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==
@@ -3596,6 +3596,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.32":
version "18.0.32"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.32.tgz#5e88b2af6833251d54ec7fe86d393224499f41d5"
@@ -6216,6 +6223,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"
@@ -11094,7 +11109,7 @@ prompts@^2.0.1, prompts@^2.4.0:
kleur "^3.0.3"
sisteransi "^1.0.5"
-prop-types@^15.0.0, prop-types@^15.7.2, prop-types@^15.8.1:
+prop-types@^15.0.0, 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==
@@ -11427,6 +11442,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"