From feb682ebda1aa8707d0662fc1a0297c33a5ede6c Mon Sep 17 00:00:00 2001 From: Cintia Sanchez Garcia Date: Mon, 4 Sep 2023 09:34:07 +0200 Subject: [PATCH] Some improvements in web application performance Signed-off-by: Cintia Sanchez Garcia --- web/src/App.css | 3 +- web/src/App.tsx | 3 +- web/src/hooks/useBreakpointDetect.ts | 2 +- web/src/hooks/useOutsideClick.ts | 2 +- web/src/layout/common/ExternalLink.tsx | 2 +- web/src/layout/common/Loading.module.css | 112 ++---------------- web/src/layout/common/Loading.tsx | 30 +++-- web/src/layout/common/Modal.module.css | 10 +- web/src/layout/common/Modal.tsx | 2 +- web/src/layout/common/Searchbar.tsx | 3 +- .../common/itemModal/ParticipationStats.tsx | 2 +- web/src/layout/common/itemModal/index.tsx | 2 +- web/src/layout/common/zoomModal/index.tsx | 7 +- web/src/layout/explore/Content.tsx | 12 +- ...andscape.module.css => Explore.module.css} | 8 ++ .../ButtonToTopScroll.module.css | 0 .../ButtonToTopScroll.tsx | 3 +- .../{cardCategory => card}/Card.module.css | 0 .../explore/{cardCategory => card}/Card.tsx | 2 +- .../explore/card/CardCategory.module.css | 3 + .../CardTitle.module.css | 0 .../{cardCategory => card}/CardTitle.tsx | 2 +- .../{cardCategory => card}/Content.module.css | 0 .../{cardCategory => card}/Content.tsx | 59 +++++---- .../{cardCategory => card}/Menu.module.css | 0 .../explore/{cardCategory => card}/Menu.tsx | 2 +- .../explore/{cardCategory => card}/index.tsx | 34 ++++-- .../explore/filters/ActiveFiltersList.tsx | 2 +- .../explore/filters/SearchbarSection.tsx | 4 +- web/src/layout/explore/filters/Section.tsx | 2 +- web/src/layout/explore/filters/index.tsx | 2 +- .../{gridCategory => grid}/Grid.module.css | 0 .../explore/{gridCategory => grid}/Grid.tsx | 2 +- .../GridCategory.module.css | 4 + .../GridItem.module.css | 0 .../{gridCategory => grid}/GridItem.tsx | 4 +- web/src/layout/explore/grid/index.tsx | 108 +++++++++++++++++ web/src/layout/explore/gridCategory/index.tsx | 83 ------------- web/src/layout/explore/index.tsx | 69 ++++++++++- web/src/styles/default.scss | 2 +- web/src/utils/areEqualProps.ts | 3 +- web/src/utils/filterData.ts | 2 +- web/src/utils/gridCategoryLayout.ts | 2 +- web/src/utils/isElementInView.ts | 2 +- web/src/utils/sortItemsByOrderValue.ts | 2 +- 45 files changed, 326 insertions(+), 272 deletions(-) rename web/src/layout/explore/{Landscape.module.css => Explore.module.css} (82%) rename web/src/layout/explore/{cardCategory => card}/ButtonToTopScroll.module.css (100%) rename web/src/layout/explore/{cardCategory => card}/ButtonToTopScroll.tsx (96%) rename web/src/layout/explore/{cardCategory => card}/Card.module.css (100%) rename web/src/layout/explore/{cardCategory => card}/Card.tsx (99%) create mode 100644 web/src/layout/explore/card/CardCategory.module.css rename web/src/layout/explore/{cardCategory => card}/CardTitle.module.css (100%) rename web/src/layout/explore/{cardCategory => card}/CardTitle.tsx (96%) rename web/src/layout/explore/{cardCategory => card}/Content.module.css (100%) rename web/src/layout/explore/{cardCategory => card}/Content.tsx (78%) rename web/src/layout/explore/{cardCategory => card}/Menu.module.css (100%) rename web/src/layout/explore/{cardCategory => card}/Menu.tsx (98%) rename web/src/layout/explore/{cardCategory => card}/index.tsx (84%) rename web/src/layout/explore/{gridCategory => grid}/Grid.module.css (100%) rename web/src/layout/explore/{gridCategory => grid}/Grid.tsx (99%) rename web/src/layout/explore/{gridCategory => grid}/GridCategory.module.css (93%) rename web/src/layout/explore/{gridCategory => grid}/GridItem.module.css (100%) rename web/src/layout/explore/{gridCategory => grid}/GridItem.tsx (98%) create mode 100644 web/src/layout/explore/grid/index.tsx delete mode 100644 web/src/layout/explore/gridCategory/index.tsx diff --git a/web/src/App.css b/web/src/App.css index 032f314a..d3965d26 100644 --- a/web/src/App.css +++ b/web/src/App.css @@ -24,7 +24,8 @@ select:focus { } .noScroll-sidebar, -.noScroll-modal { +.noScroll-modal, +.noScroll-loading { margin: 0; height: 100%; overflow: hidden; diff --git a/web/src/App.tsx b/web/src/App.tsx index 8169fc55..ff1c21dd 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,7 +1,8 @@ import './styles/default.scss'; import './App.css'; -import { isNull, isUndefined } from 'lodash'; +import isNull from 'lodash/isNull'; +import isUndefined from 'lodash/isUndefined'; import { useEffect } from 'react'; import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; diff --git a/web/src/hooks/useBreakpointDetect.ts b/web/src/hooks/useBreakpointDetect.ts index 253da887..f025882f 100644 --- a/web/src/hooks/useBreakpointDetect.ts +++ b/web/src/hooks/useBreakpointDetect.ts @@ -1,4 +1,4 @@ -import { throttle } from 'lodash'; +import throttle from 'lodash/throttle'; import { useEffect, useState } from 'react'; import { Breakpoint } from '../types'; diff --git a/web/src/hooks/useOutsideClick.ts b/web/src/hooks/useOutsideClick.ts index 1afd7da7..95a5f58e 100644 --- a/web/src/hooks/useOutsideClick.ts +++ b/web/src/hooks/useOutsideClick.ts @@ -1,4 +1,4 @@ -import { isNull } from 'lodash'; +import isNull from 'lodash/isNull'; import { RefObject, useCallback, useEffect, useState } from 'react'; export const useOutsideClick = ( diff --git a/web/src/layout/common/ExternalLink.tsx b/web/src/layout/common/ExternalLink.tsx index 72528214..325833a7 100644 --- a/web/src/layout/common/ExternalLink.tsx +++ b/web/src/layout/common/ExternalLink.tsx @@ -1,6 +1,6 @@ import './ExternalLink.module.css'; -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import { SVGIconKind } from '../../types'; import SVGIcon from './SVGIcon'; diff --git a/web/src/layout/common/Loading.module.css b/web/src/layout/common/Loading.module.css index 57ef44c5..81f92dae 100644 --- a/web/src/layout/common/Loading.module.css +++ b/web/src/layout/common/Loading.module.css @@ -1,5 +1,5 @@ .wrapper { - background-color: rgba(255, 255, 255, 0.25); + background-color: rgba(255, 255, 255, 0.5); z-index: 100; } @@ -7,110 +7,12 @@ background-color: transparent; } -.wave { - height: 50px; - width: 50px; - border-radius: 50%; - display: inline-block; - position: relative; +.spinner { + width: 3.5rem; + height: 3.5rem; } -.miniWave { - height: 12px; - width: 12px; -} - -.wave:before, -.wave:after { - content: ''; - border: 2px solid var(--light-blue); - border-radius: 50%; - width: 50px; - height: 50px; - position: absolute; - left: 0px; - right: 0px; -} - -[data-theme='dark'] .wave:before, -[data-theme='dark'] .wave:after { - border-color: var(--light-blue); -} - -.miniWave:before, -.miniWave:after { - width: 12px; - height: 12px; - border-width: 1px; -} - -.wave:before { - -webkit-transform: scale(1, 1); - -ms-transform: scale(1, 1); - transform: scale(1, 1); - opacity: 1; - -webkit-animation: spWaveBe 0.6s infinite linear; - animation: spWaveBe 0.6s infinite linear; -} - -.wave:after { - -webkit-transform: scale(0, 0); - -ms-transform: scale(0, 0); - transform: scale(0, 0); - opacity: 0; - -webkit-animation: spWaveAf 0.6s infinite linear; - animation: spWaveAf 0.6s infinite linear; -} - -@-webkit-keyframes spWaveAf { - from { - -webkit-transform: scale(0.5, 0.5); - transform: scale(0.5, 0.5); - opacity: 0; - } - to { - -webkit-transform: scale(1, 1); - transform: scale(1, 1); - opacity: 1; - } -} -@keyframes spWaveAf { - from { - -webkit-transform: scale(0.5, 0.5); - transform: scale(0.5, 0.5); - -webkit-transform: scale(0.5, 0.5); - transform: scale(0.5, 0.5); - opacity: 0; - } - to { - -webkit-transform: scale(1, 1); - transform: scale(1, 1); - -webkit-transform: scale(1, 1); - transform: scale(1, 1); - opacity: 1; - } -} -@-webkit-keyframes spWaveBe { - from { - -webkit-transform: scale(1, 1); - transform: scale(1, 1); - opacity: 1; - } - to { - -webkit-transform: scale(1.5, 1.5); - transform: scale(1.5, 1.5); - opacity: 0; - } -} -@keyframes spWaveBe { - from { - -webkit-transform: scale(1, 1); - transform: scale(1, 1); - opacity: 1; - } - to { - -webkit-transform: scale(1.5, 1.5); - transform: scale(1.5, 1.5); - opacity: 0; - } +.miniSpinner { + width: 0.5rem; + height: 0.5rem; } diff --git a/web/src/layout/common/Loading.tsx b/web/src/layout/common/Loading.tsx index ac3e6f76..c22d75d5 100644 --- a/web/src/layout/common/Loading.tsx +++ b/web/src/layout/common/Loading.tsx @@ -1,5 +1,5 @@ -import classnames from 'classnames'; -import { isUndefined } from 'lodash'; +import classNames from 'classnames'; +import isUndefined from 'lodash/isUndefined'; import React from 'react'; import styles from './Loading.module.css'; @@ -16,11 +16,15 @@ export interface ILoadingProps { export const Loading: React.FC = (props: ILoadingProps) => { const getSpinner = (): JSX.Element => { return ( -
- Loading... +
+
+ Loading... +
); }; @@ -29,8 +33,8 @@ export const Loading: React.FC = (props: ILoadingProps) => { <> {isUndefined(props.noWrapper) || !props.noWrapper ? (
= (props: ILoadingProps) => { props.className )} > -
{getSpinner()}
+
+ {getSpinner()} +
) : ( <>{getSpinner()} diff --git a/web/src/layout/common/Modal.module.css b/web/src/layout/common/Modal.module.css index e4dc0c51..0f2a55e2 100644 --- a/web/src/layout/common/Modal.module.css +++ b/web/src/layout/common/Modal.module.css @@ -1,5 +1,8 @@ .active { - animation: fadeIn 0.25s 1 ease-in-out forwards; + animation-name: fadeIn; + animation-duration: 0.15s; + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; z-index: 1080; } @@ -24,7 +27,10 @@ } .activeBackdrop { - animation: fadeIn50 0.15s 1 ease-in-out forwards; + animation-name: fadeIn50; + animation-duration: 0.15s; + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; } @keyframes fadeIn { diff --git a/web/src/layout/common/Modal.tsx b/web/src/layout/common/Modal.tsx index 628b22c6..5e304413 100644 --- a/web/src/layout/common/Modal.tsx +++ b/web/src/layout/common/Modal.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import { MouseEvent, useEffect, useRef, useState } from 'react'; import { useBodyScroll } from '../../hooks/useBodyScroll'; diff --git a/web/src/layout/common/Searchbar.tsx b/web/src/layout/common/Searchbar.tsx index cf763f90..6df2d493 100644 --- a/web/src/layout/common/Searchbar.tsx +++ b/web/src/layout/common/Searchbar.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import { isNull, isUndefined } from 'lodash'; +import isNull from 'lodash/isNull'; +import isUndefined from 'lodash/isUndefined'; import { ChangeEvent, KeyboardEvent, useContext, useEffect, useRef, useState } from 'react'; import { useOutsideClick } from '../../hooks/useOutsideClick'; diff --git a/web/src/layout/common/itemModal/ParticipationStats.tsx b/web/src/layout/common/itemModal/ParticipationStats.tsx index 88e72d57..58ee64db 100644 --- a/web/src/layout/common/itemModal/ParticipationStats.tsx +++ b/web/src/layout/common/itemModal/ParticipationStats.tsx @@ -1,4 +1,4 @@ -import { isInteger } from 'lodash'; +import isInteger from 'lodash/isInteger'; import moment from 'moment'; import { useEffect, useState } from 'react'; diff --git a/web/src/layout/common/itemModal/index.tsx b/web/src/layout/common/itemModal/index.tsx index 312e3d34..90dcf2c2 100644 --- a/web/src/layout/common/itemModal/index.tsx +++ b/web/src/layout/common/itemModal/index.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import moment from 'moment'; import { useContext, useEffect, useState } from 'react'; diff --git a/web/src/layout/common/zoomModal/index.tsx b/web/src/layout/common/zoomModal/index.tsx index 1b9a57be..226dfff5 100644 --- a/web/src/layout/common/zoomModal/index.tsx +++ b/web/src/layout/common/zoomModal/index.tsx @@ -1,4 +1,5 @@ -import { isUndefined, throttle } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; +import throttle from 'lodash/throttle'; import { useContext, useEffect, useRef, useState } from 'react'; import { COLORS } from '../../../data'; @@ -14,7 +15,7 @@ import { ZoomContext, ZoomProps, } from '../../context/AppContext'; -import GridItem from '../../explore/gridCategory/GridItem'; +import GridItem from '../../explore/grid/GridItem'; import FullScreenModal from '../FullScreenModal'; import { Loading } from '../Loading'; import styles from './ZoomModal.module.css'; @@ -127,7 +128,7 @@ const ZoomModal = () => {
) : (
- +
)} diff --git a/web/src/layout/explore/Content.tsx b/web/src/layout/explore/Content.tsx index 9e9f8c7b..5e443505 100644 --- a/web/src/layout/explore/Content.tsx +++ b/web/src/layout/explore/Content.tsx @@ -4,14 +4,15 @@ import { ViewMode } from '../../types'; import arePropsEqual from '../../utils/areEqualProps'; import { CategoriesData } from '../../utils/prepareData'; import { ViewModeContext, ViewModeProps } from '../context/AppContext'; -import CardCategory from './cardCategory'; -import GridCategory from './gridCategory'; +import CardCategory from './card'; +import GridCategory from './grid'; interface Props { isSelected: boolean; containerWidth: number; data: CategoriesData; categories_overridden?: string[]; + finishLoading: () => void; } // Memoized version of content to avoid unnecessary @@ -20,18 +21,21 @@ const Content = memo(function Content(props: Props) { return ( <> -
+
-
+
diff --git a/web/src/layout/explore/Landscape.module.css b/web/src/layout/explore/Explore.module.css similarity index 82% rename from web/src/layout/explore/Landscape.module.css rename to web/src/layout/explore/Explore.module.css index 51dfb8f2..2bce4936 100644 --- a/web/src/layout/explore/Landscape.module.css +++ b/web/src/layout/explore/Explore.module.css @@ -33,3 +33,11 @@ .btnGroupLegend { font-size: 0.75rem; } + +.container { + min-height: 400px; +} + +.loadingContent { + min-height: calc(100vh - 92px - 36px); +} diff --git a/web/src/layout/explore/cardCategory/ButtonToTopScroll.module.css b/web/src/layout/explore/card/ButtonToTopScroll.module.css similarity index 100% rename from web/src/layout/explore/cardCategory/ButtonToTopScroll.module.css rename to web/src/layout/explore/card/ButtonToTopScroll.module.css diff --git a/web/src/layout/explore/cardCategory/ButtonToTopScroll.tsx b/web/src/layout/explore/card/ButtonToTopScroll.tsx similarity index 96% rename from web/src/layout/explore/cardCategory/ButtonToTopScroll.tsx rename to web/src/layout/explore/card/ButtonToTopScroll.tsx index 5e184159..b2820ce7 100644 --- a/web/src/layout/explore/cardCategory/ButtonToTopScroll.tsx +++ b/web/src/layout/explore/card/ButtonToTopScroll.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; -import { isNull, throttle } from 'lodash'; +import isNull from 'lodash/isNull'; +import throttle from 'lodash/throttle'; import { MouseEvent, useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; diff --git a/web/src/layout/explore/cardCategory/Card.module.css b/web/src/layout/explore/card/Card.module.css similarity index 100% rename from web/src/layout/explore/cardCategory/Card.module.css rename to web/src/layout/explore/card/Card.module.css diff --git a/web/src/layout/explore/cardCategory/Card.tsx b/web/src/layout/explore/card/Card.tsx similarity index 99% rename from web/src/layout/explore/cardCategory/Card.tsx rename to web/src/layout/explore/card/Card.tsx index b70fe471..67390129 100644 --- a/web/src/layout/explore/cardCategory/Card.tsx +++ b/web/src/layout/explore/card/Card.tsx @@ -1,4 +1,4 @@ -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import { Item, Repository, SVGIconKind } from '../../../types'; import cleanEmojis from '../../../utils/cleanEmojis'; diff --git a/web/src/layout/explore/card/CardCategory.module.css b/web/src/layout/explore/card/CardCategory.module.css new file mode 100644 index 00000000..1675eb90 --- /dev/null +++ b/web/src/layout/explore/card/CardCategory.module.css @@ -0,0 +1,3 @@ +.loading { + min-height: calc(100vh - 92px - 36px); +} diff --git a/web/src/layout/explore/cardCategory/CardTitle.module.css b/web/src/layout/explore/card/CardTitle.module.css similarity index 100% rename from web/src/layout/explore/cardCategory/CardTitle.module.css rename to web/src/layout/explore/card/CardTitle.module.css diff --git a/web/src/layout/explore/cardCategory/CardTitle.tsx b/web/src/layout/explore/card/CardTitle.tsx similarity index 96% rename from web/src/layout/explore/cardCategory/CardTitle.tsx rename to web/src/layout/explore/card/CardTitle.tsx index 29475a84..f5ce10b5 100644 --- a/web/src/layout/explore/cardCategory/CardTitle.tsx +++ b/web/src/layout/explore/card/CardTitle.tsx @@ -1,4 +1,4 @@ -import { throttle } from 'lodash'; +import throttle from 'lodash/throttle'; import { useEffect, useRef, useState } from 'react'; import styles from './CardTitle.module.css'; diff --git a/web/src/layout/explore/cardCategory/Content.module.css b/web/src/layout/explore/card/Content.module.css similarity index 100% rename from web/src/layout/explore/cardCategory/Content.module.css rename to web/src/layout/explore/card/Content.module.css diff --git a/web/src/layout/explore/cardCategory/Content.tsx b/web/src/layout/explore/card/Content.tsx similarity index 78% rename from web/src/layout/explore/cardCategory/Content.tsx rename to web/src/layout/explore/card/Content.tsx index 370c3a95..d09b28e3 100644 --- a/web/src/layout/explore/cardCategory/Content.tsx +++ b/web/src/layout/explore/card/Content.tsx @@ -1,4 +1,4 @@ -import { orderBy } from 'lodash'; +import orderBy from 'lodash/orderBy'; import { memo, useContext } from 'react'; import { useNavigate } from 'react-router-dom'; import { Waypoint } from 'react-waypoint'; @@ -17,29 +17,27 @@ interface Props { data: CategoriesData; isVisible: boolean; } -const Content = memo(function Content(props: Props) { - const navigate = useNavigate(); - const { updateActiveItemId } = useContext(AppActionsContext) as ActionsContext; - const sortItems = (firstCategory: string, firstSubcategory: string): BaseItem[] => { - return orderBy( - props.data[firstCategory][firstSubcategory].items, - [(item: BaseItem) => item.name.toString()], - 'asc' - ); - }; +interface WaypointProps { + id: string; + children: JSX.Element; + isVisible: boolean; +} - const handleEnter = (id: string) => { - if (`#${id}` !== location.hash) { +const WaypointItem = (props: WaypointProps) => { + const navigate = useNavigate(); + + const handleEnter = () => { + if (`#${props.id}` !== location.hash && props.isVisible) { navigate( - { ...location, hash: id }, + { ...location, hash: props.id }, { replace: true, } ); - if (!isElementInView(`btn_${id}`)) { - const target = window.document.getElementById(`btn_${id}`); + if (!isElementInView(`btn_${props.id}`)) { + const target = window.document.getElementById(`btn_${props.id}`); if (target) { target.scrollIntoView({ block: 'nearest' }); } @@ -47,6 +45,24 @@ const Content = memo(function Content(props: Props) { } }; + return ( + + {props.children} + + ); +}; + +const Content = memo(function Content(props: Props) { + const { updateActiveItemId } = useContext(AppActionsContext) as ActionsContext; + + const sortItems = (firstCategory: string, firstSubcategory: string): BaseItem[] => { + return orderBy( + props.data[firstCategory][firstSubcategory].items, + [(item: BaseItem) => item.name.toString()], + 'asc' + ); + }; + return ( <> {Object.keys(props.menu).map((cat: string) => { @@ -54,14 +70,9 @@ const Content = memo(function Content(props: Props) {
{props.menu[cat].map((subcat: string) => { const id = convertStringSpaces(`${cat}/${subcat}`); + return ( - handleEnter(id)} - fireOnRapidScroll={false} - > +
{cat} / {subcat} @@ -81,7 +92,7 @@ const Content = memo(function Content(props: Props) { })}
-
+ ); })}
diff --git a/web/src/layout/explore/cardCategory/Menu.module.css b/web/src/layout/explore/card/Menu.module.css similarity index 100% rename from web/src/layout/explore/cardCategory/Menu.module.css rename to web/src/layout/explore/card/Menu.module.css diff --git a/web/src/layout/explore/cardCategory/Menu.tsx b/web/src/layout/explore/card/Menu.tsx similarity index 98% rename from web/src/layout/explore/cardCategory/Menu.tsx rename to web/src/layout/explore/card/Menu.tsx index 9538f59b..c3854964 100644 --- a/web/src/layout/explore/cardCategory/Menu.tsx +++ b/web/src/layout/explore/card/Menu.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import { useLayoutEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router'; diff --git a/web/src/layout/explore/cardCategory/index.tsx b/web/src/layout/explore/card/index.tsx similarity index 84% rename from web/src/layout/explore/cardCategory/index.tsx rename to web/src/layout/explore/card/index.tsx index b27d0f89..3843d907 100644 --- a/web/src/layout/explore/cardCategory/index.tsx +++ b/web/src/layout/explore/card/index.tsx @@ -1,4 +1,5 @@ -import { isEqual, isUndefined } from 'lodash'; +import isEqual from 'lodash/isEqual'; +import isUndefined from 'lodash/isUndefined'; import { memo, useContext, useEffect, useLayoutEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -9,7 +10,6 @@ import goToElement from '../../../utils/goToElement'; import { SubcategoryDetails } from '../../../utils/gridCategoryLayout'; import isElementInView from '../../../utils/isElementInView'; import { CategoriesData } from '../../../utils/prepareData'; -import { Loading } from '../../common/Loading'; import { FullDataContext, FullDataProps } from '../../context/AppContext'; import ButtonToTopScroll from './ButtonToTopScroll'; import Content from './Content'; @@ -19,6 +19,7 @@ interface Props { isVisible: boolean; data: CategoriesData; categories_overridden?: string[]; + finishLoading: () => void; } const TITLE_OFFSET = 16; @@ -26,6 +27,7 @@ const TITLE_OFFSET = 16; const CardCategory = memo(function CardCategory(props: Props) { const { fullDataReady } = useContext(FullDataContext) as FullDataProps; const navigate = useNavigate(); + const [firstLoad, setFirstLoad] = useState(props.isVisible); const [menu, setMenu] = useState(); const [initialFullRender, setInitialFullRender] = useState(false); @@ -130,16 +132,30 @@ const CardCategory = memo(function CardCategory(props: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [menu, props.isVisible, fullDataReady]); + useEffect(() => { + if (props.isVisible) { + props.finishLoading(); + if (!firstLoad) { + setFirstLoad(true); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.isVisible]); + if (isUndefined(menu)) return null; return ( -
- -
- {fullDataReady ? : } - -
-
+ <> + {firstLoad && ( +
+ +
+ {fullDataReady && } + +
+
+ )} + ); }, arePropsEqual); diff --git a/web/src/layout/explore/filters/ActiveFiltersList.tsx b/web/src/layout/explore/filters/ActiveFiltersList.tsx index 7cf53d30..6e8df983 100644 --- a/web/src/layout/explore/filters/ActiveFiltersList.tsx +++ b/web/src/layout/explore/filters/ActiveFiltersList.tsx @@ -1,4 +1,4 @@ -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import { memo } from 'react'; import { ActiveFilters, FilterCategory, SVGIconKind } from '../../../types'; diff --git a/web/src/layout/explore/filters/SearchbarSection.tsx b/web/src/layout/explore/filters/SearchbarSection.tsx index 00c7d976..8f5d5d28 100644 --- a/web/src/layout/explore/filters/SearchbarSection.tsx +++ b/web/src/layout/explore/filters/SearchbarSection.tsx @@ -1,4 +1,6 @@ -import { isNull, isUndefined, sortBy } from 'lodash'; +import isNull from 'lodash/isNull'; +import isUndefined from 'lodash/isUndefined'; +import sortBy from 'lodash/sortBy'; import { ChangeEvent, memo, useCallback, useEffect, useRef, useState } from 'react'; import { FilterCategory, FilterOption, FilterSection, SVGIconKind } from '../../../types'; diff --git a/web/src/layout/explore/filters/Section.tsx b/web/src/layout/explore/filters/Section.tsx index 079bf07b..8ea8a2cb 100644 --- a/web/src/layout/explore/filters/Section.tsx +++ b/web/src/layout/explore/filters/Section.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import { ChangeEvent, Fragment, memo, useCallback } from 'react'; import { FilterCategory, FilterOption, FilterSection } from '../../../types'; diff --git a/web/src/layout/explore/filters/index.tsx b/web/src/layout/explore/filters/index.tsx index 0396286c..69cabda5 100644 --- a/web/src/layout/explore/filters/index.tsx +++ b/web/src/layout/explore/filters/index.tsx @@ -1,4 +1,4 @@ -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import { memo, MouseEvent, useCallback, useEffect, useState } from 'react'; import { FILTERS } from '../../../data'; diff --git a/web/src/layout/explore/gridCategory/Grid.module.css b/web/src/layout/explore/grid/Grid.module.css similarity index 100% rename from web/src/layout/explore/gridCategory/Grid.module.css rename to web/src/layout/explore/grid/Grid.module.css diff --git a/web/src/layout/explore/gridCategory/Grid.tsx b/web/src/layout/explore/grid/Grid.tsx similarity index 99% rename from web/src/layout/explore/gridCategory/Grid.tsx rename to web/src/layout/explore/grid/Grid.tsx index 980bcb53..4e3dec60 100644 --- a/web/src/layout/explore/gridCategory/Grid.tsx +++ b/web/src/layout/explore/grid/Grid.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import { memo, useContext, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; diff --git a/web/src/layout/explore/gridCategory/GridCategory.module.css b/web/src/layout/explore/grid/GridCategory.module.css similarity index 93% rename from web/src/layout/explore/gridCategory/GridCategory.module.css rename to web/src/layout/explore/grid/GridCategory.module.css index 3f55a8a0..1ec00322 100644 --- a/web/src/layout/explore/gridCategory/GridCategory.module.css +++ b/web/src/layout/explore/grid/GridCategory.module.css @@ -46,3 +46,7 @@ gap: var(--card-gap); /* justify-content: center; */ } + +.loading { + min-height: calc(100vh - 92px - 36px); +} diff --git a/web/src/layout/explore/gridCategory/GridItem.module.css b/web/src/layout/explore/grid/GridItem.module.css similarity index 100% rename from web/src/layout/explore/gridCategory/GridItem.module.css rename to web/src/layout/explore/grid/GridItem.module.css diff --git a/web/src/layout/explore/gridCategory/GridItem.tsx b/web/src/layout/explore/grid/GridItem.tsx similarity index 98% rename from web/src/layout/explore/gridCategory/GridItem.tsx rename to web/src/layout/explore/grid/GridItem.tsx index 9e8e8ac9..9c286ff6 100644 --- a/web/src/layout/explore/gridCategory/GridItem.tsx +++ b/web/src/layout/explore/grid/GridItem.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import { memo, useContext, useEffect, useRef, useState } from 'react'; import { BaseItem, Item } from '../../../types'; @@ -7,7 +7,7 @@ import arePropsEqual from '../../../utils/areEqualProps'; import Image from '../../common/Image'; import { Loading } from '../../common/Loading'; import { ActionsContext, AppActionsContext, FullDataContext, FullDataProps } from '../../context/AppContext'; -import Card from '../cardCategory/Card'; +import Card from '../card/Card'; import styles from './GridItem.module.css'; interface Props { diff --git a/web/src/layout/explore/grid/index.tsx b/web/src/layout/explore/grid/index.tsx new file mode 100644 index 00000000..6bd38c42 --- /dev/null +++ b/web/src/layout/explore/grid/index.tsx @@ -0,0 +1,108 @@ +import classNames from 'classnames'; +import isUndefined from 'lodash/isUndefined'; +import { memo, useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; + +import { SVGIconKind } from '../../../types'; +import arePropsEqual from '../../../utils/areEqualProps'; +import generateColorsArray from '../../../utils/generateColorsArray'; +import { SubcategoryDetails } from '../../../utils/gridCategoryLayout'; +import { CategoriesData } from '../../../utils/prepareData'; +import SVGIcon from '../../common/SVGIcon'; +import Grid from './Grid'; +import styles from './GridCategory.module.css'; + +interface Props { + containerWidth: number; + data: CategoriesData; + categories_overridden?: string[]; + isVisible: boolean; + finishLoading: () => void; +} + +const GridCategory = memo(function GridCategory(props: Props) { + const colorsList = generateColorsArray(Object.keys(props.data).length); + const [firstLoad, setFirstLoad] = useState(props.isVisible); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (props.isVisible !== isVisible) { + setIsVisible(props.isVisible); + if (props.isVisible) { + props.finishLoading(); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.isVisible]); + + useEffect(() => { + if (!firstLoad && isVisible) { + setFirstLoad(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVisible]); + + return ( + <> + {firstLoad && ( + <> + {Object.keys(props.data).map((cat: string, index: number) => { + const isOverriden = !isUndefined(props.categories_overridden) && props.categories_overridden.includes(cat); + const subcategories: SubcategoryDetails[] = []; + Object.keys(props.data[cat]).forEach((subcat: string) => { + if (props.data[cat][subcat].items.length !== 0) { + subcategories.push({ + name: subcat, + itemsCount: props.data[cat][subcat].itemsCount, + itemsFeaturedCount: props.data[cat][subcat].itemsFeaturedCount, + }); + } + }); + + if (subcategories.length === 0) return null; + + return ( +
+
+
+
{cat}
+ +
+ + + +
+
+
+ +
+ +
+
+ ); + })} + + )} + + ); +}, arePropsEqual); + +export default GridCategory; diff --git a/web/src/layout/explore/gridCategory/index.tsx b/web/src/layout/explore/gridCategory/index.tsx deleted file mode 100644 index 36a24e4d..00000000 --- a/web/src/layout/explore/gridCategory/index.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import classNames from 'classnames'; -import { isUndefined } from 'lodash'; -import { memo } from 'react'; -import { Link } from 'react-router-dom'; - -import { SVGIconKind } from '../../../types'; -import arePropsEqual from '../../../utils/areEqualProps'; -import generateColorsArray from '../../../utils/generateColorsArray'; -import { SubcategoryDetails } from '../../../utils/gridCategoryLayout'; -import { CategoriesData } from '../../../utils/prepareData'; -import SVGIcon from '../../common/SVGIcon'; -import Grid from './Grid'; -import styles from './GridCategory.module.css'; - -interface Props { - containerWidth: number; - data: CategoriesData; - categories_overridden?: string[]; -} - -const GridCategory = memo(function GridCategory(props: Props) { - const colorsList = generateColorsArray(Object.keys(props.data).length); - - return ( - <> - {Object.keys(props.data).map((cat: string, index: number) => { - const isOverriden = !isUndefined(props.categories_overridden) && props.categories_overridden.includes(cat); - const subcategories: SubcategoryDetails[] = []; - Object.keys(props.data[cat]).forEach((subcat: string) => { - if (props.data[cat][subcat].items.length !== 0) { - subcategories.push({ - name: subcat, - itemsCount: props.data[cat][subcat].itemsCount, - itemsFeaturedCount: props.data[cat][subcat].itemsFeaturedCount, - }); - } - }); - - if (subcategories.length === 0) return null; - - return ( -
-
-
-
{cat}
- -
- - - -
-
-
- -
- -
-
- ); - })} - - ); -}, arePropsEqual); - -export default GridCategory; diff --git a/web/src/layout/explore/index.tsx b/web/src/layout/explore/index.tsx index b94c7cc2..b89fc4be 100644 --- a/web/src/layout/explore/index.tsx +++ b/web/src/layout/explore/index.tsx @@ -1,14 +1,17 @@ import classNames from 'classnames'; -import { isUndefined, throttle } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; +import throttle from 'lodash/throttle'; import { Fragment, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { GROUP_PARAM, VIEW_MODE_PARAM } from '../../data'; +import { useBodyScroll } from '../../hooks/useBodyScroll'; import { ActiveFilters, BaseData, BaseItem, FilterCategory, Group, Item, SVGIconKind, ViewMode } from '../../types'; import countVisibleItems from '../../utils/countVisibleItems'; import filterData from '../../utils/filterData'; import itemsDataGetter from '../../utils/itemsDataGetter'; import prepareData, { GroupData } from '../../utils/prepareData'; +import { Loading } from '../common/Loading'; import NoData from '../common/NoData'; import SVGIcon from '../common/SVGIcon'; import { @@ -22,14 +25,18 @@ import { ZoomLevelProps, } from '../context/AppContext'; import Content from './Content'; +import styles from './Explore.module.css'; import Filters from './filters'; import ActiveFiltersList from './filters/ActiveFiltersList'; -import styles from './Landscape.module.css'; interface Props { data: BaseData; } +export type LoadedContent = { + [key in ViewMode]: string[]; +}; + const TITLE_GAP = 40; const Landscape = (props: Props) => { @@ -51,6 +58,42 @@ const Landscape = (props: Props) => { const [activeFilters, setActiveFilters] = useState({}); const [groupsData, setGroupsData] = useState(prepareData(props.data, visibleItems)); const [numVisibleItems, setNumVisibleItems] = useState(); + const [visibleLoading, setVisibleLoading] = useState(true); + + const getLoadedData = (): LoadedContent => { + const data: LoadedContent = { [ViewMode.Card]: [], [ViewMode.Grid]: [] }; + + if (selectedViewMode) { + data[selectedViewMode] = [selectedGroup || 'default']; + } + + return data; + }; + + const [loaded, setLoaded] = useState(getLoadedData()); + + useBodyScroll(visibleLoading, 'loading'); + + const checkIfVisibleLoading = (viewMode?: ViewMode, groupName?: string) => { + if (viewMode) { + const group = groupName || selectedGroup || 'default'; + if (!loaded[viewMode].includes(groupName || 'default')) { + setVisibleLoading(true); + setLoaded({ + ...loaded, + [viewMode]: [...loaded[viewMode], group], + }); + } else { + setVisibleLoading(false); + } + } else { + setVisibleLoading(false); + } + }; + + const finishLoading = useCallback(() => { + setVisibleLoading(false); + }, []); const updateQueryString = (param: string, value: string) => { const updatedSearchParams = new URLSearchParams(searchParams); @@ -58,7 +101,7 @@ const Landscape = (props: Props) => { updatedSearchParams.set(param, value); navigate( - { ...location, search: updatedSearchParams.toString(), hash: '' }, + { ...location, search: updatedSearchParams.toString(), hash: undefined }, { replace: true, } @@ -177,6 +220,7 @@ const Landscape = (props: Props) => { !isUndefined(selectedGroup) && group.name === selectedGroup, })} onClick={() => { + checkIfVisibleLoading(selectedViewMode, group.name); setSelectedGroup(group.name); updateQueryString(GROUP_PARAM, group.name); }} @@ -207,6 +251,7 @@ const Landscape = (props: Props) => { })} onClick={() => { if (!isActive) { + checkIfVisibleLoading(value, selectedGroup); updateViewMode(value); updateQueryString(VIEW_MODE_PARAM, value); } @@ -276,19 +321,30 @@ const Landscape = (props: Props) => {
)} -
-
+
+
+ {visibleLoading && } + {props.data.groups ? ( <> {props.data.groups.map((group: Group) => { const isSelected = selectedGroup === group.name; return ( -
+
); @@ -300,6 +356,7 @@ const Landscape = (props: Props) => { containerWidth={containerWidth} data={groupsData.default} categories_overridden={props.data.categories_overridden} + finishLoading={finishLoading} /> )}
diff --git a/web/src/styles/default.scss b/web/src/styles/default.scss index 0799f236..650b0b7a 100644 --- a/web/src/styles/default.scss +++ b/web/src/styles/default.scss @@ -90,7 +90,7 @@ $font-family-sans-serif: @import '../node_modules/bootstrap/scss/tooltip'; @import '../node_modules/bootstrap/scss/popover'; // @import "../node_modules/bootstrap/scss/carousel"; -// @import "../node_modules/bootstrap/scss/spinners"; +@import '../node_modules/bootstrap/scss/spinners'; @import '../node_modules/bootstrap/scss/offcanvas'; @import '../node_modules/bootstrap/scss/placeholders'; diff --git a/web/src/utils/areEqualProps.ts b/web/src/utils/areEqualProps.ts index 17a809e8..3806c5ee 100644 --- a/web/src/utils/areEqualProps.ts +++ b/web/src/utils/areEqualProps.ts @@ -1,4 +1,5 @@ -import { isEqual, reduce } from 'lodash'; +import isEqual from 'lodash/isEqual'; +import reduce from 'lodash/reduce'; interface Props { [key: string]: unknown; diff --git a/web/src/utils/filterData.ts b/web/src/utils/filterData.ts index 6634a349..61ef4001 100644 --- a/web/src/utils/filterData.ts +++ b/web/src/utils/filterData.ts @@ -1,4 +1,4 @@ -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import { ActiveFilters, FilterCategory, Item, Repository } from '../types'; diff --git a/web/src/utils/gridCategoryLayout.ts b/web/src/utils/gridCategoryLayout.ts index 2580003f..ddce231f 100644 --- a/web/src/utils/gridCategoryLayout.ts +++ b/web/src/utils/gridCategoryLayout.ts @@ -1,4 +1,4 @@ -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; import { CSSProperties } from 'react'; // Input used to calculate the grid category layout. diff --git a/web/src/utils/isElementInView.ts b/web/src/utils/isElementInView.ts index 21413e7f..28c35f72 100644 --- a/web/src/utils/isElementInView.ts +++ b/web/src/utils/isElementInView.ts @@ -1,4 +1,4 @@ -import { isNull } from 'lodash'; +import isNull from 'lodash/isNull'; const isElementInView = (id: string) => { try { diff --git a/web/src/utils/sortItemsByOrderValue.ts b/web/src/utils/sortItemsByOrderValue.ts index befb7d45..bc3c9324 100644 --- a/web/src/utils/sortItemsByOrderValue.ts +++ b/web/src/utils/sortItemsByOrderValue.ts @@ -1,4 +1,4 @@ -import { sortBy } from 'lodash'; +import sortBy from 'lodash/sortBy'; import { BaseItem } from '../types';