From b2984147b36799a5aa2c65b66cbc52e8cdc96d2e Mon Sep 17 00:00:00 2001 From: Cintia Sanchez Garcia Date: Wed, 30 Aug 2023 15:03:34 +0200 Subject: [PATCH] Some improvements in web application context Signed-off-by: Cintia Sanchez Garcia --- web/src/layout/common/Searchbar.tsx | 4 +- web/src/layout/common/itemModal/index.tsx | 21 +++-- web/src/layout/common/zoomModal/index.tsx | 33 +++++--- web/src/layout/context/AppContext.tsx | 83 ++++++++++++++----- .../layout/explore/cardCategory/Content.tsx | 11 +-- web/src/layout/explore/cardCategory/index.tsx | 4 +- web/src/layout/explore/gridCategory/Grid.tsx | 6 +- .../layout/explore/gridCategory/GridItem.tsx | 27 ++---- web/src/layout/explore/index.tsx | 24 ++++-- web/src/utils/itemsDataGetter.ts | 22 ++--- 10 files changed, 146 insertions(+), 89 deletions(-) diff --git a/web/src/layout/common/Searchbar.tsx b/web/src/layout/common/Searchbar.tsx index 46e62173..cf763f90 100644 --- a/web/src/layout/common/Searchbar.tsx +++ b/web/src/layout/common/Searchbar.tsx @@ -4,7 +4,7 @@ import { ChangeEvent, KeyboardEvent, useContext, useEffect, useRef, useState } f import { useOutsideClick } from '../../hooks/useOutsideClick'; import { BaseItem, SVGIconKind } from '../../types'; -import { AppContext, Context } from '../context/AppContext'; +import { ActionsContext, AppActionsContext } from '../context/AppContext'; import HoverableItem from './HoverableItem'; import MaturityBadge from './MaturityBadge'; import styles from './Searchbar.module.css'; @@ -18,7 +18,7 @@ const SEARCH_DELAY = 3 * 100; // 300ms const MIN_CHARACTERS_SEARCH = 2; const Searchbar = (props: Props) => { - const { updateActiveItemId } = useContext(AppContext) as Context; + const { updateActiveItemId } = useContext(AppActionsContext) as ActionsContext; const inputEl = useRef(null); const dropdownRef = useRef(null); const [value, setValue] = useState(''); diff --git a/web/src/layout/common/itemModal/index.tsx b/web/src/layout/common/itemModal/index.tsx index 3de7a581..312e3d34 100644 --- a/web/src/layout/common/itemModal/index.tsx +++ b/web/src/layout/common/itemModal/index.tsx @@ -8,7 +8,14 @@ import cleanEmojis from '../../../utils/cleanEmojis'; import formatProfitLabel from '../../../utils/formatLabelProfit'; import itemsDataGetter from '../../../utils/itemsDataGetter'; import prettifyNumber from '../../../utils/prettifyNumber'; -import { AppContext, Context } from '../../context/AppContext'; +import { + ActionsContext, + AppActionsContext, + FullDataContext, + FullDataProps, + ItemContext, + ItemProps, +} from '../../context/AppContext'; import ExternalLink from '../ExternalLink'; import Image from '../Image'; import { Loading } from '../Loading'; @@ -19,7 +26,9 @@ import styles from './ItemModal.module.css'; import ParticipationStats from './ParticipationStats'; const ItemModal = () => { - const { activeItemId, updateActiveItemId, fullDataReady } = useContext(AppContext) as Context; + const { fullDataReady } = useContext(FullDataContext) as FullDataProps; + const { visibleItemId } = useContext(ItemContext) as ItemProps; + const { updateActiveItemId } = useContext(AppActionsContext) as ActionsContext; const [itemInfo, setItemInfo] = useState(undefined); let description = 'This item does not have a description available yet'; let stars: number | undefined; @@ -75,20 +84,20 @@ const ItemModal = () => { async function fetchItemInfo() { try { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - setItemInfo(await itemsDataGetter.get(activeItemId!)); + setItemInfo(await itemsDataGetter.findById(visibleItemId!)); } catch { setItemInfo(null); } } - if (activeItemId && fullDataReady) { + if (visibleItemId && fullDataReady) { fetchItemInfo(); } else { setItemInfo(undefined); } - }, [activeItemId, fullDataReady]); + }, [visibleItemId, fullDataReady]); - if (isUndefined(activeItemId)) return null; + if (isUndefined(visibleItemId)) return null; return ( updateActiveItemId()}> diff --git a/web/src/layout/common/zoomModal/index.tsx b/web/src/layout/common/zoomModal/index.tsx index 29a7baa6..1b9a57be 100644 --- a/web/src/layout/common/zoomModal/index.tsx +++ b/web/src/layout/common/zoomModal/index.tsx @@ -6,7 +6,14 @@ import { BaseItem, Item } from '../../../types'; import { calculateItemsPerRow, calculateWidthInPx } from '../../../utils/gridCategoryLayout'; import itemsDataGetter from '../../../utils/itemsDataGetter'; import sortItemsByOrderValue from '../../../utils/sortItemsByOrderValue'; -import { AppContext, Context } from '../../context/AppContext'; +import { + ActionsContext, + AppActionsContext, + FullDataContext, + FullDataProps, + ZoomContext, + ZoomProps, +} from '../../context/AppContext'; import GridItem from '../../explore/gridCategory/GridItem'; import FullScreenModal from '../FullScreenModal'; import { Loading } from '../Loading'; @@ -16,7 +23,9 @@ const GAP = 96 + 40; // Padding | Title const CARD_WIDTH = 75; const ZoomModal = () => { - const { activeSection, updateActiveSection, fullDataReady } = useContext(AppContext) as Context; + const { visibleZoomView } = useContext(ZoomContext) as ZoomProps; + const { fullDataReady } = useContext(FullDataContext) as FullDataProps; + const { updateActiveSection } = useContext(AppActionsContext) as ActionsContext; const modal = useRef(null); const container = useRef(null); const [items, setItems] = useState(); @@ -38,24 +47,24 @@ const ZoomModal = () => { async function fetchItems() { try { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - setItems(await itemsDataGetter.filterItemsBySection(activeSection!)); + setItems(await itemsDataGetter.filterItemsBySection(visibleZoomView!)); } catch { setItems(null); } } - if (activeSection && fullDataReady) { + if (visibleZoomView && fullDataReady) { fetchItems(); } else { setItems(undefined); } - }, [activeSection, fullDataReady]); + }, [visibleZoomView, fullDataReady]); useEffect(() => { - if (container && container.current && activeSection) { + if (container && container.current && visibleZoomView) { setContainerWidth(checkNumColumns(container.current.offsetWidth - GAP)); } - }, [container, activeSection]); + }, [container, visibleZoomView]); useEffect(() => { const checkContainerWidth = throttle(() => { @@ -72,7 +81,7 @@ const ZoomModal = () => { return () => window.removeEventListener('resize', checkContainerWidth); }, []); - if (isUndefined(activeSection)) return null; + if (isUndefined(visibleZoomView)) return null; return ( updateActiveSection()}> @@ -86,10 +95,10 @@ const ZoomModal = () => { >
-
{activeSection.category}
+
{visibleZoomView.category}
@@ -99,9 +108,9 @@ const ZoomModal = () => { >
-
{activeSection.subcategory}
+
{visibleZoomView.subcategory}
diff --git a/web/src/layout/context/AppContext.tsx b/web/src/layout/context/AppContext.tsx index b3dfc1ad..57b913be 100644 --- a/web/src/layout/context/AppContext.tsx +++ b/web/src/layout/context/AppContext.tsx @@ -1,13 +1,22 @@ -import { createContext, useState } from 'react'; +import { createContext, useCallback, useMemo, useState } from 'react'; import itemsDataGetter from '../../utils/itemsDataGetter'; -export type Context = { - activeItemId?: string; +export type FullDataProps = { + fullDataReady: boolean; +}; + +export type ItemProps = { + visibleItemId?: string; +}; + +export type ZoomProps = { + visibleZoomView?: ActiveSection; +}; + +export type ActionsContext = { updateActiveItemId: (itemId?: string) => void; - activeSection?: ActiveSection; updateActiveSection: (activeSection?: ActiveSection) => void; - fullDataReady: boolean; }; export interface ActiveSection { @@ -20,33 +29,67 @@ interface Props { children: JSX.Element; } -export const AppContext = createContext(null); +export const FullDataContext = createContext(null); +export const ItemContext = createContext(null); +export const ZoomContext = createContext(null); +export const AppActionsContext = createContext(null); const AppContextProvider = (props: Props) => { - const [activeItemId, setActiveItemId] = useState(); - const [activeSection, setActiveSection] = useState(); + const [visibleItemId, setVisibleItemId] = useState(); + const [visibleZoomView, setVisibleZoomView] = useState(); const [fullDataReady, setFullDataReady] = useState(false); - const updateActiveItemId = (id?: string) => { - setActiveItemId(id); - }; + const updateActiveItemId = useCallback((id?: string) => { + setVisibleItemId(id); + }, []); - const updateActiveSection = (section?: ActiveSection) => { - setActiveSection(section); - }; + const updateActiveSection = useCallback((section?: ActiveSection) => { + setVisibleZoomView(section); + }, []); + + const fullDataValue = useMemo( + () => ({ + fullDataReady, + }), + [fullDataReady] + ); + + const itemValue = useMemo( + () => ({ + visibleItemId, + }), + [visibleItemId] + ); + + const zoomValue = useMemo( + () => ({ + visibleZoomView, + }), + [visibleZoomView] + ); + + const contextActionsValue = useMemo( + () => ({ + updateActiveItemId, + updateActiveSection, + }), + [updateActiveItemId, updateActiveSection] + ); - itemsDataGetter.isReady({ + itemsDataGetter.subscribe({ updateStatus: (status: boolean) => { setFullDataReady(status); }, }); return ( - - {props.children} - + + + + {props.children} + + + ); }; diff --git a/web/src/layout/explore/cardCategory/Content.tsx b/web/src/layout/explore/cardCategory/Content.tsx index d43930ab..370c3a95 100644 --- a/web/src/layout/explore/cardCategory/Content.tsx +++ b/web/src/layout/explore/cardCategory/Content.tsx @@ -1,13 +1,14 @@ import { orderBy } from 'lodash'; -import { useContext } from 'react'; +import { memo, useContext } from 'react'; import { useNavigate } from 'react-router-dom'; import { Waypoint } from 'react-waypoint'; import { BaseItem, CardMenu, Item } from '../../../types'; +import arePropsEqual from '../../../utils/areEqualProps'; import convertStringSpaces from '../../../utils/convertStringSpaces'; import isElementInView from '../../../utils/isElementInView'; import { CategoriesData } from '../../../utils/prepareData'; -import { AppContext, Context } from '../../context/AppContext'; +import { ActionsContext, AppActionsContext } from '../../context/AppContext'; import Card from './Card'; import styles from './Content.module.css'; @@ -16,9 +17,9 @@ interface Props { data: CategoriesData; isVisible: boolean; } -const Content = (props: Props) => { +const Content = memo(function Content(props: Props) { const navigate = useNavigate(); - const { updateActiveItemId } = useContext(AppContext) as Context; + const { updateActiveItemId } = useContext(AppActionsContext) as ActionsContext; const sortItems = (firstCategory: string, firstSubcategory: string): BaseItem[] => { return orderBy( @@ -88,6 +89,6 @@ const Content = (props: Props) => { })} ); -}; +}, arePropsEqual); export default Content; diff --git a/web/src/layout/explore/cardCategory/index.tsx b/web/src/layout/explore/cardCategory/index.tsx index bb5aa4c7..b27d0f89 100644 --- a/web/src/layout/explore/cardCategory/index.tsx +++ b/web/src/layout/explore/cardCategory/index.tsx @@ -10,7 +10,7 @@ import { SubcategoryDetails } from '../../../utils/gridCategoryLayout'; import isElementInView from '../../../utils/isElementInView'; import { CategoriesData } from '../../../utils/prepareData'; import { Loading } from '../../common/Loading'; -import { AppContext, Context } from '../../context/AppContext'; +import { FullDataContext, FullDataProps } from '../../context/AppContext'; import ButtonToTopScroll from './ButtonToTopScroll'; import Content from './Content'; import Menu from './Menu'; @@ -24,7 +24,7 @@ interface Props { const TITLE_OFFSET = 16; const CardCategory = memo(function CardCategory(props: Props) { - const { fullDataReady } = useContext(AppContext) as Context; + const { fullDataReady } = useContext(FullDataContext) as FullDataProps; const navigate = useNavigate(); const [menu, setMenu] = useState(); const [initialFullRender, setInitialFullRender] = useState(false); diff --git a/web/src/layout/explore/gridCategory/Grid.tsx b/web/src/layout/explore/gridCategory/Grid.tsx index 5be22576..688ba096 100644 --- a/web/src/layout/explore/gridCategory/Grid.tsx +++ b/web/src/layout/explore/gridCategory/Grid.tsx @@ -15,7 +15,7 @@ import getGridCategoryLayout, { import { SubcategoryData } from '../../../utils/prepareData'; import sortItemsByOrderValue from '../../../utils/sortItemsByOrderValue'; import SVGIcon from '../../common/SVGIcon'; -import { AppContext, Context } from '../../context/AppContext'; +import { ActionsContext, AppActionsContext } from '../../context/AppContext'; import styles from './Grid.module.css'; import GridItem from './GridItem'; @@ -31,7 +31,7 @@ interface Props { } const Grid = memo(function Grid(props: Props) { - const { updateActiveSection } = useContext(AppContext) as Context; + const { updateActiveSection } = useContext(AppActionsContext) as ActionsContext; const [grid, setGrid] = useState(); useEffect(() => { @@ -117,8 +117,8 @@ const Grid = memo(function Grid(props: Props) { {sortedItems.map((item: BaseItem | Item) => { return ( diff --git a/web/src/layout/explore/gridCategory/GridItem.tsx b/web/src/layout/explore/gridCategory/GridItem.tsx index 54596912..9e8e8ac9 100644 --- a/web/src/layout/explore/gridCategory/GridItem.tsx +++ b/web/src/layout/explore/gridCategory/GridItem.tsx @@ -1,11 +1,12 @@ import classNames from 'classnames'; import { isUndefined } from 'lodash'; -import { useContext, useEffect, useRef, useState } from 'react'; +import { memo, useContext, useEffect, useRef, useState } from 'react'; import { BaseItem, Item } from '../../../types'; +import arePropsEqual from '../../../utils/areEqualProps'; import Image from '../../common/Image'; import { Loading } from '../../common/Loading'; -import { AppContext, Context } from '../../context/AppContext'; +import { ActionsContext, AppActionsContext, FullDataContext, FullDataProps } from '../../context/AppContext'; import Card from '../cardCategory/Card'; import styles from './GridItem.module.css'; @@ -18,26 +19,16 @@ interface Props { const DEFAULT_DROPDOWN_WIDTH = 450; const DEFAULT_MARGIN = 30; -const GridItem = (props: Props) => { +const GridItem = memo(function GridItem(props: Props) { const ref = useRef(null); const wrapper = useRef(null); - const { updateActiveItemId } = useContext(AppContext) as Context; + const { fullDataReady } = useContext(FullDataContext) as FullDataProps; + const { updateActiveItemId } = useContext(AppActionsContext) as ActionsContext; const [visibleDropdown, setVisibleDropdown] = useState(false); const [onLinkHover, setOnLinkHover] = useState(false); const [onDropdownHover, setOnDropdownHover] = useState(false); const [tooltipAlignment, setTooltipAlignment] = useState<'right' | 'left' | 'center'>('center'); const [elWidth, setElWidth] = useState(0); - const [fullVersion, setFullVersion] = useState(isUndefined(props.item.has_repositories)); - const [hasRepositories, setHasRepositories] = useState( - props.item.has_repositories || 'repositories' in props.item - ); - - useEffect(() => { - if (isUndefined(props.item.has_repositories)) { - setFullVersion(true); - setHasRepositories('repositories' in props.item); - } - }, [props.item]); useEffect(() => { const calculateTooltipPosition = () => { @@ -90,7 +81,7 @@ const GridItem = (props: Props) => { { [`border-2 ${styles.bigCard}`]: props.item.featured }, { [styles.withLabel]: props.item.featured && props.item.featured.label }, { - [styles.withRepo]: hasRepositories, + [styles.withRepo]: 'repositories' in props.item || props.item.has_repositories, } )} > @@ -114,7 +105,7 @@ const GridItem = (props: Props) => { }} >
- {!fullVersion ? : } + {!fullDataReady ? : }
)}
@@ -156,6 +147,6 @@ const GridItem = (props: Props) => { ); -}; +}, arePropsEqual); export default GridItem; diff --git a/web/src/layout/explore/index.tsx b/web/src/layout/explore/index.tsx index 6b74abc7..dd8d7da1 100644 --- a/web/src/layout/explore/index.tsx +++ b/web/src/layout/explore/index.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { isUndefined, throttle } from 'lodash'; -import { Fragment, useCallback, useEffect, useRef, useState } from 'react'; +import { Fragment, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { DEFAULT_VIEW_MODE, DEFAULT_ZOOM_LEVELS, GROUP_PARAM, VIEW_MODE_PARAM, ZOOM_LEVELS } from '../../data'; @@ -22,6 +22,7 @@ import itemsDataGetter from '../../utils/itemsDataGetter'; import prepareData, { GroupData } from '../../utils/prepareData'; import NoData from '../common/NoData'; import SVGIcon from '../common/SVGIcon'; +import { FullDataContext, FullDataProps } from '../context/AppContext'; import Content from './Content'; import Filters from './filters'; import ActiveFiltersList from './filters/ActiveFiltersList'; @@ -37,6 +38,7 @@ const Landscape = (props: Props) => { const navigate = useNavigate(); const location = useLocation(); const point = useBreakpointDetect(); + const { fullDataReady } = useContext(FullDataContext) as FullDataProps; const [searchParams] = useSearchParams(); const container = useRef(null); const [containerWidth, setContainerWidth] = useState(0); @@ -56,12 +58,6 @@ const Landscape = (props: Props) => { const [groupsData, setGroupsData] = useState(prepareData(props.data, visibleItems)); const [numVisibleItems, setNumVisibleItems] = useState(); - itemsDataGetter.subscribe({ - updateLandscapeData: (items: Item[]) => { - setLandscapeData(items); - }, - }); - const updateQueryString = (param: string, value: string) => { const updatedSearchParams = new URLSearchParams(searchParams); updatedSearchParams.delete(param); @@ -75,6 +71,20 @@ const Landscape = (props: Props) => { ); }; + useEffect(() => { + async function fetchItems() { + try { + setLandscapeData(await itemsDataGetter.getAll()); + } catch { + setLandscapeData(undefined); + } + } + + if (fullDataReady) { + fetchItems(); + } + }, [fullDataReady]); + useEffect(() => { if (landscapeData) { setVisibleItems(landscapeData); diff --git a/web/src/utils/itemsDataGetter.ts b/web/src/utils/itemsDataGetter.ts index d1ec2852..695bf395 100644 --- a/web/src/utils/itemsDataGetter.ts +++ b/web/src/utils/itemsDataGetter.ts @@ -1,25 +1,16 @@ import { ActiveSection } from '../layout/context/AppContext'; import { Item, LandscapeData } from '../types'; -export interface ItemsDataHandler { - updateLandscapeData(items: Item[]): void; -} - export interface ItemsDataStatus { updateStatus(status: boolean): void; } export class ItemsDataGetter { - private updateData?: ItemsDataHandler; private updateStatus?: ItemsDataStatus; public ready = false; public landscapeData?: LandscapeData; - public subscribe(updateData: ItemsDataHandler) { - this.updateData = updateData; - } - - public isReady(updateStatus: ItemsDataStatus) { + public subscribe(updateStatus: ItemsDataStatus) { this.updateStatus = updateStatus; } @@ -30,9 +21,6 @@ export class ItemsDataGetter { .then((data) => { this.landscapeData = data; this.ready = true; - if (this.updateData) { - this.updateData.updateLandscapeData(data.items); - } if (this.updateStatus) { this.updateStatus.updateStatus(true); } @@ -40,7 +28,13 @@ export class ItemsDataGetter { } } - public async get(id: string): Promise { + public async getAll(): Promise { + if (this.ready && this.landscapeData && this.landscapeData.items) { + return this.landscapeData.items; + } + } + + public async findById(id: string): Promise { if (this.ready && this.landscapeData && this.landscapeData.items) { return this.landscapeData.items.find((i: Item) => id === i.id); }