diff --git a/.eslintrc b/.eslintrc index f1b1cc9e..a7e5165c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -39,6 +39,7 @@ "curly": "error", "@typescript-eslint/indent": "off", "@typescript-eslint/brace-style": "off", + "import/prefer-default-export": "off", "no-underscore-dangle": [ "error", { diff --git a/package.json b/package.json index 01aae47f..0f8cb0c6 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@emotion/server": "11.11.0", "@emotion/styled": "11.13.0", "@graasp/query-client": "5.0.0", - "@graasp/sdk": "4.32.1", + "@graasp/sdk": "4.33.0", "@graasp/stylis-plugin-rtl": "2.2.0", "@graasp/translations": "1.40.0", "@graasp/ui": "5.4.0", diff --git a/src/components/collection/Collection.tsx b/src/components/collection/Collection.tsx index 3b785dff..93082dfc 100644 --- a/src/components/collection/Collection.tsx +++ b/src/components/collection/Collection.tsx @@ -4,7 +4,7 @@ import { validate } from 'uuid'; import { useContext, useEffect } from 'react'; -import { Box } from '@mui/material'; +import { Box, Skeleton } from '@mui/material'; import { AccountType, @@ -27,11 +27,7 @@ type Props = { }; const Collection = ({ id }: Props) => { const { hooks, mutations } = useContext(QueryClientContext); - const { - data: collection, - isLoading: isLoadingItem, - isError, - } = hooks.useItem(id); + const { data: collection, isLoading: isLoadingItem } = hooks.useItem(id); const { data: currentMember } = hooks.useCurrentMember(); // get item published const { @@ -68,47 +64,50 @@ const Collection = ({ id }: Props) => { ); } - if (isError) { + if (currentMember?.type === AccountType.Guest) { + return null; + } + if (collection) { return ( - - - + <> + + + + + ); } - if (currentMember?.type === AccountType.Guest) { - return null; + if (isLoadingItem) { + return ; } return ( - <> - - - - - + + + ); }; diff --git a/src/components/collection/summary/Summary.tsx b/src/components/collection/summary/Summary.tsx index df9ee5df..3c5ececa 100644 --- a/src/components/collection/summary/Summary.tsx +++ b/src/components/collection/summary/Summary.tsx @@ -16,7 +16,7 @@ import SummaryDetails from './SummaryDetails'; import SummaryHeader from './SummaryHeader'; type SummaryProps = { - collection?: DiscriminatedItem; + collection: DiscriminatedItem; publishedRoot?: ItemPublished | null; isLoading: boolean; totalViews: number; diff --git a/src/components/collection/summary/SummaryDetails.tsx b/src/components/collection/summary/SummaryDetails.tsx index 1e00fca3..6c3e6d63 100644 --- a/src/components/collection/summary/SummaryDetails.tsx +++ b/src/components/collection/summary/SummaryDetails.tsx @@ -18,6 +18,7 @@ import { DiscriminatedItem, formatDate, } from '@graasp/sdk'; +import { DEFAULT_LANG, langs } from '@graasp/translations'; import { CATEGORY_COLORS, UrlSearch } from '../../../config/constants'; import { @@ -90,7 +91,7 @@ const CategoryDisplay = ({ }; type SummaryDetailsProps = { - collection?: DiscriminatedItem; + collection: DiscriminatedItem; publishedRootItem?: DiscriminatedItem; lang: string; isLoading: boolean; @@ -126,10 +127,11 @@ const SummaryDetails: React.FC = ({ ?.filter((c) => c.category.type === CategoryType.Discipline) ?.map((c) => c.category); - // TODO: should use item language - const languages = itemCategories - ?.filter((c) => c.category.type === CategoryType.Language) - ?.map((c) => c.category); + let langValue = langs[DEFAULT_LANG]; + if (collection.lang in langs) { + // @ts-ignore + langValue = langs[collection.lang]; + } return ( = ({ - {t(LIBRARY.COLLECTION_LANGUAGES_TITLE)} + {t(LIBRARY.COLLECTION_LANGUAGE_TITLE)} - {languages ? ( - - ) : ( - - )} + diff --git a/src/components/filters/CategoryFilter.tsx b/src/components/filters/CategoryFilter.tsx new file mode 100644 index 00000000..7bfafa61 --- /dev/null +++ b/src/components/filters/CategoryFilter.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +import { Category } from '@graasp/sdk'; + +import { useCategoriesTranslation } from '../../config/i18n'; +import { buildSearchFilterCategoryId } from '../../config/selectors'; +import { Filter } from './Filter'; + +type FilterProps = { + category: string; + title: string; + options?: Category[]; + // IDs of selected options. + selectedOptions: string[]; + onOptionChange: (key: string, newValue: boolean) => void; + onClearOptions: () => void; + isLoading: boolean; +}; + +// eslint-disable-next-line react/function-component-definition +export function CategoryFilter({ + category, + title, + onOptionChange, + onClearOptions, + options, + selectedOptions, + isLoading, +}: FilterProps) { + const { t: translateCategories } = useCategoriesTranslation(); + + return ( + [c.id, translateCategories(c.name)])} + selectedOptions={selectedOptions} + onOptionChange={onOptionChange} + onClearOptions={onClearOptions} + /> + ); +} diff --git a/src/components/filters/Filter.tsx b/src/components/filters/Filter.tsx new file mode 100644 index 00000000..5f6a3e79 --- /dev/null +++ b/src/components/filters/Filter.tsx @@ -0,0 +1,136 @@ +import { useEffect, useRef, useState } from 'react'; + +import { ExpandMoreRounded } from '@mui/icons-material'; +import { Box, Button, Skeleton, Stack, Typography } from '@mui/material'; + +import { GRAASP_COLOR } from '../../config/constants'; +import { useLibraryTranslation } from '../../config/i18n'; +import { buildSearchFilterPopperButtonId } from '../../config/selectors'; +import LIBRARY from '../../langs/constants'; +import { FilterPopper, FilterPopperProps } from './FilterPopper'; + +type FilterProps = { + title: string; + // IDs of selected options. + selectedOptions: string[]; + isLoading?: boolean; + onClearOptions: FilterPopperProps['onClearOptions']; + onOptionChange: FilterPopperProps['onOptionChange']; + id: string; + options?: FilterPopperProps['options']; +}; + +// eslint-disable-next-line import/prefer-default-export +export const Filter = ({ + title, + selectedOptions, + isLoading, + onClearOptions, + onOptionChange, + id, + options, +}: FilterProps) => { + const { t } = useLibraryTranslation(); + const [showPopper, setShowPopper] = useState(false); + const togglePopper = () => { + setShowPopper((oldVal) => !oldVal); + }; + + const popperAnchor = useRef(null); + const popper = useRef(null); + + const onDocumentScrolled = () => { + setShowPopper(() => false); + }; + + const onDocumentClicked = (event: MouseEvent) => { + if ( + !popper.current?.contains(event.target as Node) && + !popperAnchor.current?.contains(event.target as Node) + ) { + setShowPopper(() => false); + } + }; + // Listens for clicks outside of the popper to dismiss it when we click outside. + useEffect(() => { + if (showPopper) { + document.addEventListener('click', onDocumentClicked); + document.addEventListener('scroll', onDocumentScrolled); + } + return () => { + document.removeEventListener('click', onDocumentClicked); + document.removeEventListener('scroll', onDocumentScrolled); + }; + }, [showPopper]); + + const content = isLoading ? ( + + ) : ( + + ); + + return ( + + + {title} + + + {content} + + + + ); +}; diff --git a/src/components/filters/FilterHeader.tsx b/src/components/filters/FilterHeader.tsx index a986db0c..5056e9b7 100644 --- a/src/components/filters/FilterHeader.tsx +++ b/src/components/filters/FilterHeader.tsx @@ -2,23 +2,19 @@ import groupBy from 'lodash.groupby'; import React, { FC, useContext, useEffect, useRef, useState } from 'react'; -import { ExpandMoreRounded } from '@mui/icons-material'; import { Box, - Button, Checkbox, Container, Divider, FormControlLabel, - Skeleton, Stack, Typography, styled, } from '@mui/material'; -import { Category, CategoryType } from '@graasp/sdk'; +import { CategoryType } from '@graasp/sdk'; -import { GRAASP_COLOR } from '../../config/constants'; import { useCategoriesTranslation, useLibraryTranslation, @@ -26,155 +22,12 @@ import { import { ALL_COLLECTIONS_TITLE_ID, ENABLE_IN_DEPTH_SEARCH_CHECKBOX_ID, - buildSearchFilterCategoryId, - buildSearchFilterPopperButtonId, } from '../../config/selectors'; import LIBRARY from '../../langs/constants'; import { QueryClientContext } from '../QueryClientContext'; import Search from '../search/Search'; -import FilterPopper from './FilterPopper'; - -type FilterProps = { - category: string; - title: string; - options?: Category[]; - // IDs of selected options. - selectedOptions: string[]; - onOptionChange: (key: string, newValue: boolean) => void; - onClearOptions: () => void; - isLoading: boolean; -}; - -const Filter: React.FC = ({ - category, - title, - onOptionChange, - onClearOptions, - options, - selectedOptions, - isLoading, -}) => { - const { t: translateCategories } = useCategoriesTranslation(); - const { t } = useLibraryTranslation(); - const [showPopper, setShowPopper] = useState(false); - const togglePopper = () => { - setShowPopper((oldVal) => !oldVal); - }; - - const popperAnchor = useRef(null); - const popper = useRef(null); - - const onDocumentScrolled = () => { - setShowPopper(() => false); - }; - - const onDocumentClicked = (event: MouseEvent) => { - if ( - !popper.current?.contains(event.target as Node) && - !popperAnchor.current?.contains(event.target as Node) - ) { - setShowPopper(() => false); - } - }; - - const selectionCount = React.useMemo( - () => - selectedOptions.filter((id) => options?.find((opt) => opt.id === id)) - .length, - [selectedOptions, options], - ); - - const selectionStr = React.useMemo(() => { - const optionsStr = - options - ?.filter((it) => selectedOptions.includes(it.id)) - .map((it) => translateCategories(it.name))?.[0] ?? - t(LIBRARY.FILTER_DROPDOWN_NO_FILTER); - return optionsStr; - }, [selectedOptions, options]); - - // Listens for clicks outside of the popper to dismiss it when we click outside. - useEffect(() => { - if (showPopper) { - document.addEventListener('click', onDocumentClicked); - document.addEventListener('scroll', onDocumentScrolled); - } - return () => { - document.removeEventListener('click', onDocumentClicked); - document.removeEventListener('scroll', onDocumentScrolled); - }; - }, [showPopper]); - - const content = isLoading ? ( - - ) : ( - - ); - - return ( - - - {title} - - - {content} - - - - ); -}; +import { CategoryFilter } from './CategoryFilter'; +import { LangFilter } from './LangFilter'; const StyledFilterContainer = styled(Stack)(() => ({ backgroundColor: 'white', @@ -212,17 +65,21 @@ type FilterHeaderProps = { searchPreset?: string; categoryPreset?: string[][]; isLoadingResults: boolean; + setLangs: (langs: string[]) => void; + langs: string[]; }; const FilterHeader: FC = ({ onFiltersChanged, onChangeSearch, + setLangs, onSearch, searchPreset, categoryPreset, isLoadingResults, onIncludeContentChange, shouldIncludeContent, + langs, }) => { const { t: translateCategories } = useCategoriesTranslation(); const { t } = useLibraryTranslation(); @@ -239,26 +96,6 @@ const FilterHeader: FC = ({ const allCategories = groupBy(categories, (entry) => entry.type); const levelList = allCategories[CategoryType.Level]; const disciplineList = allCategories[CategoryType.Discipline]; - const languageList = allCategories[CategoryType.Language]; - - // TODO: Replace with real values. - // const licenseList: List = convertJs([ - // { - // id: '3f811e5f-5221-4d22-a20c-1086af809bda', - // name: 'Public Domain (CC0)', - // type: '3f811e5f-5221-4d22-a20c-1086af809bd0', - // }, - // { - // id: '3f811e5f-5221-4d22-a20c-1086af809bdb', - // name: 'For Commercial Use', - // type: '3f811e5f-5221-4d22-a20c-1086af809bd0', - // }, - // { - // id: '3f811e5f-5221-4d22-a20c-1086af809bdc', - // name: 'Derivable', - // type: '3f811e5f-5221-4d22-a20c-1086af809bd0', - // }, - // ]); useEffect(() => { setSelectedFilters(categoryPreset ? categoryPreset.flat() : []); @@ -322,47 +159,40 @@ const FilterHeader: FC = ({ /> ); + const selectedDisciplineOptions = selectedFilters.filter((id) => + disciplineList?.find((opt) => opt.id === id), + ); + const selectedLevelOptions = selectedFilters.filter((id) => + levelList?.find((opt) => opt.id === id), + ); + const filters = [ - onClearCategory(levelList?.map((l) => l.id))} isLoading={isCategoriesLoading} />, - onClearCategory(disciplineList?.map((d) => d.id))} isLoading={isCategoriesLoading} />, - onClearCategory(languageList?.map((d) => d.id))} - isLoading={isCategoriesLoading} + selectedOptions={langs} + setLangs={setLangs} />, - // onClearCategory(licenseList?.map((d) => d.id))} - // isLoading={isCategoriesLoading} - // />, ]; return ( diff --git a/src/components/filters/FilterPopper.tsx b/src/components/filters/FilterPopper.tsx index b1f7d3f5..4149ea86 100644 --- a/src/components/filters/FilterPopper.tsx +++ b/src/components/filters/FilterPopper.tsx @@ -8,17 +8,11 @@ import { Grow, Popper, Stack, - Typography, styled, } from '@mui/material'; import { TransitionProps as MUITransitionProps } from '@mui/material/transitions'; -import { Category } from '@graasp/sdk'; - -import { - useCategoriesTranslation, - useLibraryTranslation, -} from '../../config/i18n'; +import { useLibraryTranslation } from '../../config/i18n'; import { CLEAR_FILTER_POPPER_BUTTON_ID, FILTER_POPPER_ID, @@ -35,29 +29,28 @@ const StyledPopper = styled(Stack)(() => ({ boxShadow: '0 2px 15px rgba(0, 0, 0, 0.08)', })); -type FilterPopperProps = { +export type FilterPopperProps = { open: boolean; anchorEl: HTMLElement | null; - options?: Category[]; + options?: [k: string, v: string][]; // IDs of selected options. selectedOptions: string[]; onOptionChange: (id: string, newSelected: boolean) => void; onClearOptions: () => void; }; -const FilterPopper = React.forwardRef( +export const FilterPopper = React.forwardRef( ( { + options, anchorEl, onOptionChange, open, - options, selectedOptions, onClearOptions, }, ref, ) => { - const { t: translateCategories } = useCategoriesTranslation(); const { t } = useLibraryTranslation(); return ( ( // eslint-disable-next-line react/jsx-props-no-spreading - {options - ?.map((c) => ({ ...c, name: translateCategories(c.name) })) - ?.sort(compare) - .map((option, idx) => { - const isSelected = selectedOptions.includes(option.id); - return ( - - - - onOptionChange(option.id, !isSelected) - } - /> - } - label={option.name} - labelPlacement="end" - /> - - - ); - }) || ( - - {t(LIBRARY.FILTER_DROPDOWN_NO_CATEGORIES_AVAILABLE)} - - )} + {options?.sort(compare).map(([k, v], idx) => { + const isSelected = selectedOptions.includes(k); + return ( + + + onOptionChange(k, !isSelected)} + /> + } + label={v} + labelPlacement="end" + /> + + + ); + })}