UNPKG

@selfcommunity/react-ui

Version:

React UI Components to integrate a Community created with SelfCommunity Platform.

234 lines (228 loc) • 15.7 kB
import { __rest } from "tslib"; import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { Box, Button, Chip, FormControl, Grid, Icon, IconButton, InputAdornment, Stack, styled, TextField, Typography, useMediaQuery, useTheme, useThemeProps } from '@mui/material'; import { Endpoints, http } from '@selfcommunity/api-services'; import { SCPreferences, SCUserContext, useSCPreferences } from '@selfcommunity/react-core'; import { Logger } from '@selfcommunity/utils'; import classNames from 'classnames'; import PubSub from 'pubsub-js'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { SCOPE_SC_UI } from '../../constants/Errors'; import { DEFAULT_PAGINATION_OFFSET } from '../../constants/Pagination'; import { SCCourseEventType, SCTopicType } from '../../constants/PubSub'; import Course, { CourseSkeleton } from '../Course'; import Skeleton from '../Courses/Skeleton'; import { PREFIX } from './constants'; import { SCCourseTemplateType } from '../../types/course'; import CategoryAutocomplete from '../CategoryAutocomplete'; import CourseCreatePlaceholder from '../Course/CreatePlaceholder'; import InfiniteScroll from '../../shared/InfiniteScroll'; import HiddenPlaceholder from '../../shared/HiddenPlaceholder'; const classes = { root: `${PREFIX}-root`, category: `${PREFIX}-category`, courses: `${PREFIX}-courses`, emptyBox: `${PREFIX}-empty-box`, emptyIcon: `${PREFIX}-empty-icon`, emptyRotatedBox: `${PREFIX}-empty-rotated-box`, filters: `${PREFIX}-filters`, item: `${PREFIX}-item`, itemPlaceholder: `${PREFIX}-item-placeholder`, noResults: `${PREFIX}-no-results`, search: `${PREFIX}-search`, studentEmptyView: `${PREFIX}-student-empty-view`, teacherEmptyView: `${PREFIX}-teacher-empty-view`, endMessage: `${PREFIX}-end-message` }; const Root = styled(Box, { name: PREFIX, slot: 'Root' })(() => ({})); export const CoursesChipRoot = styled(Chip, { name: PREFIX, slot: 'CoursesChipRoot', shouldForwardProp: (prop) => prop !== 'showMine' && prop !== 'showManagedCourses' })(() => ({})); /** * > API documentation for the Community-JS Courses component. Learn about the available props and the CSS API. * * * The Courses component renders the list of all available courses. * Take a look at our <strong>demo</strong> component [here](/docs/sdk/community-js/react-ui/Components/Courses) #### Import ```jsx import {Courses} from '@selfcommunity/react-ui'; ``` #### Component Name The name `SCCourses` can be used when providing style overrides in the theme. #### CSS |Rule Name|Global class|Description| |---|---|---| |root|.SCCourses-root|Styles applied to the root element.| |category|.SCCourses-category|Styles applied to the category autocomplete element.| |courses|.SCCourses-courses|Styles applied to the courses section.| |emptyBox|.SCCourses-empty-box|Styles applied to the empty box element.| |emptyIcon|.SCCourses-empty-icon|Styles applied to the empty icon element.| |emptyRotatedBox|.SCCourses-empty-rotated-box|Styles applied to the rotated empty box element.| |filters|.SCCourses-filters|Styles applied to the filters section.| |item|.SCCourses-item|Styles applied to an individual item.| |itemPlaceholder|.SCCourses-item-placeholder|Styles applied to the placeholder for an item.| |noResults|.SCCourses-no-results|Styles applied when there are no results.| |search|.SCCourses-search|Styles applied to the search element.| |studentEmptyView|.SCCourses-student-empty-view|Styles applied to the student empty view.| |teacherEmptyView|.SCCourses-teacher-empty-view|Styles applied to the teacher empty view.| * @param inProps */ export default function Courses(inProps) { var _a; // PROPS const props = useThemeProps({ props: inProps, name: PREFIX }); const { endpointQueryParams = { limit: 8, offset: DEFAULT_PAGINATION_OFFSET }, className, CourseComponentProps = {}, CoursesSkeletonComponentProps = {}, CourseSkeletonComponentProps = { template: SCCourseTemplateType.PREVIEW }, CreateCourseButtonComponentProps = {}, GridContainerComponentProps = {}, GridItemComponentProps = {}, showFilters = true, filters } = props, rest = __rest(props, ["endpointQueryParams", "className", "CourseComponentProps", "CoursesSkeletonComponentProps", "CourseSkeletonComponentProps", "CreateCourseButtonComponentProps", "GridContainerComponentProps", "GridItemComponentProps", "showFilters", "filters"]); // STATE const [courses, setCourses] = useState([]); const [loading, setLoading] = useState(true); const [next, setNext] = useState(null); const [query, setQuery] = useState(''); const [_categories, setCategories] = useState([]); const [showMine, setShowMine] = useState(false); const [showManagedCourses, setShowManagedCourses] = useState(false); // CONTEXT const scUserContext = useContext(SCUserContext); const { preferences } = useSCPreferences(); // MEMO const contentAvailability = SCPreferences.CONFIGURATIONS_CONTENT_AVAILABILITY in preferences && preferences[SCPreferences.CONFIGURATIONS_CONTENT_AVAILABILITY].value; const onlyStaffEnabled = useMemo(() => { var _a; return (_a = preferences[SCPreferences.CONFIGURATIONS_COURSES_ONLY_STAFF_ENABLED]) === null || _a === void 0 ? void 0 : _a.value; }, [preferences]); const canCreateCourse = useMemo(() => { var _a, _b; return (_b = (_a = scUserContext === null || scUserContext === void 0 ? void 0 : scUserContext.user) === null || _a === void 0 ? void 0 : _a.permission) === null || _b === void 0 ? void 0 : _b.create_course; }, [(_a = scUserContext === null || scUserContext === void 0 ? void 0 : scUserContext.user) === null || _a === void 0 ? void 0 : _a.permission]); // CONST const authUserId = scUserContext.user ? scUserContext.user.id : null; const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); // REFS const updatesSubscription = useRef(null); // HANDLERS const handleChipClick = () => { setShowMine(!showMine); }; const handleDeleteClick = () => { setShowMine(false); }; /** * Fetches courses list */ const fetchCourses = () => { return http .request({ url: Endpoints.SearchCourses.url({}), method: Endpoints.SearchCourses.method, params: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, endpointQueryParams), (_categories.length && { categories: JSON.stringify(_categories) })), (query && { search: query })), (showManagedCourses && { statuses: JSON.stringify(['creator', 'manager']) })), (showMine && { statuses: JSON.stringify(['joined', 'manager']) })) }) .then((res) => { setCourses(res.data.results); setNext(res.data.next); setLoading(false); }) .catch((error) => { Logger.error(SCOPE_SC_UI, error); }); }; /** * On mount, fetches courses list */ useEffect(() => { if (!contentAvailability && !authUserId) { return; } else { fetchCourses(); } }, [contentAvailability, authUserId, showMine, showManagedCourses, _categories]); /** * Subscriber for pubsub callback */ const onDeleteCourseHandler = useCallback((_msg, deleted) => { setCourses((prev) => { if (prev.some((e) => e.id === deleted)) { return prev.filter((e) => e.id !== deleted); } return prev; }); }, [courses]); /** * On mount, subscribe to receive course updates (only delete) */ useEffect(() => { if (courses) { updatesSubscription.current = PubSub.subscribe(`${SCTopicType.COURSE}.${SCCourseEventType.DELETE}`, onDeleteCourseHandler); } return () => { updatesSubscription.current && PubSub.unsubscribe(updatesSubscription.current); }; }, [courses]); const handleNext = useMemo(() => () => { if (!next) { return; } return http .request({ url: next, method: Endpoints.SearchCourses.method }) .then((res) => { setCourses([...courses, ...res.data.results]); setNext(res.data.next); }) .catch((error) => console.log(error)) .then(() => setLoading(false)); }, [next]); /** * Handle change filter name * @param course */ const handleOnChangeFilterName = (course) => { setQuery(course.target.value); }; /** * Handle change category * @param categories */ const handleOnChangeCategory = (categories) => { const categoriesIds = categories.map((item) => item.id); setCategories(categoriesIds); }; const handleScrollUp = () => { window.scrollTo({ left: 0, top: 0, behavior: 'smooth' }); }; /** * Renders courses list */ const c = (_jsxs(_Fragment, { children: [showFilters && (_jsx(Grid, Object.assign({ container: true, className: classes.filters, gap: 2 }, { children: filters ? (filters) : (_jsxs(_Fragment, { children: [_jsx(Grid, Object.assign({ item: true, xs: 12, md: 3 }, { children: _jsx(TextField, { className: classes.search, size: 'small', fullWidth: true, value: query, label: _jsx(FormattedMessage, { id: "ui.courses.filterByName", defaultMessage: "ui.courses.filterByName" }), variant: "outlined", onChange: handleOnChangeFilterName, disabled: loading, onKeyUp: (e) => { e.preventDefault(); if (e.key === 'Enter') { fetchCourses(); } }, InputProps: { endAdornment: (_jsx(InputAdornment, Object.assign({ position: "end" }, { children: isMobile ? (_jsx(IconButton, Object.assign({ onClick: () => fetchCourses(), disabled: loading }, { children: _jsx(Icon, { children: "search" }) }))) : (_jsx(Button, { size: "small", variant: "contained", color: "secondary", onClick: () => fetchCourses(), endIcon: _jsx(Icon, { children: "search" }), disabled: loading })) }))) } }) })), authUserId && ((onlyStaffEnabled && canCreateCourse) || !onlyStaffEnabled) && (_jsx(Grid, Object.assign({ item: true }, { children: _jsx(CoursesChipRoot, { color: showManagedCourses ? 'primary' : 'default', variant: showManagedCourses ? 'filled' : 'outlined', label: _jsx(FormattedMessage, { id: "ui.courses.filterByManagedByMe", defaultMessage: "ui.courses.filterByManagedByMe" }), onClick: () => setShowManagedCourses(!showManagedCourses), // @ts-expect-error this is needed to use showMine into SCCourses showManagedCourses: showManagedCourses, deleteIcon: showManagedCourses ? _jsx(Icon, { children: "close" }) : null, onDelete: showManagedCourses ? () => setShowManagedCourses(false) : null, disabled: loading || showMine }) }))), _jsx(Grid, Object.assign({ item: true, xs: 12, md: "auto" }, { children: _jsx(FormControl, Object.assign({ fullWidth: true }, { children: _jsx(CategoryAutocomplete, { onChange: handleOnChangeCategory, className: classes.category, size: "small", multiple: true }) })) })), authUserId && (_jsx(Grid, Object.assign({ item: true }, { children: _jsx(CoursesChipRoot, { color: showMine ? 'primary' : 'default', variant: showMine ? 'filled' : 'outlined', label: _jsx(FormattedMessage, { id: "ui.courses.filterByMine", defaultMessage: "ui.courses.filterByMine" }), onClick: handleChipClick, // @ts-expect-error this is needed to use showMine into SCCourses showMine: showMine, deleteIcon: showMine ? _jsx(Icon, { children: "close" }) : null, onDelete: showMine ? handleDeleteClick : null, disabled: loading || showManagedCourses }) })))] })) }))), _jsx(_Fragment, { children: !courses.length ? (_jsx(Box, Object.assign({ className: classes.noResults }, { children: !canCreateCourse && onlyStaffEnabled ? (_jsxs(Stack, Object.assign({ className: classes.studentEmptyView }, { children: [_jsx(Stack, Object.assign({ className: classes.emptyBox }, { children: _jsx(Stack, Object.assign({ className: classes.emptyRotatedBox }, { children: _jsx(Icon, Object.assign({ className: classes.emptyIcon, color: "disabled", fontSize: "large" }, { children: "courses" })) })) })), _jsx(Typography, Object.assign({ variant: "h5", textAlign: "center" }, { children: _jsx(FormattedMessage, { id: "ui.courses.empty.title", defaultMessage: "ui.courses.empty.title" }) })), _jsx(Typography, Object.assign({ variant: "body1", textAlign: "center" }, { children: _jsx(FormattedMessage, { id: "ui.courses.empty.info", defaultMessage: "ui.courses.empty.info" }) }))] }))) : (_jsx(Box, Object.assign({ className: classes.teacherEmptyView }, { children: _jsx(Skeleton, Object.assign({ teacherView: (onlyStaffEnabled && canCreateCourse) || !onlyStaffEnabled, coursesNumber: 1 }, CoursesSkeletonComponentProps, { CourseSkeletonProps: CourseSkeletonComponentProps })) }))) }))) : (_jsx(InfiniteScroll, Object.assign({ dataLength: courses.length, next: handleNext, hasMoreNext: Boolean(next), loaderNext: isMobile ? (_jsx(CourseSkeleton, { template: SCCourseTemplateType.PREVIEW })) : (_jsx(Skeleton, Object.assign({ coursesNumber: 4 }, CoursesSkeletonComponentProps, { CourseSkeletonProps: CourseSkeletonComponentProps }))), endMessage: _jsx(Typography, Object.assign({ component: "div", className: classes.endMessage }, { children: _jsx(FormattedMessage, { id: "ui.courses.endMessage", defaultMessage: "ui.courses.endMessage", values: { // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore button: (chunk) => (_jsx(Button, Object.assign({ color: "secondary", variant: "text", onClick: handleScrollUp }, { children: chunk }))) } }) })) }, { children: _jsx(Grid, Object.assign({ container: true, spacing: { xs: 3 }, className: classes.courses }, GridContainerComponentProps, { children: _jsxs(_Fragment, { children: [courses.map((course) => (_jsx(Grid, Object.assign({ item: true, xs: 12, sm: 12, md: 6, lg: 3, className: classes.item }, GridItemComponentProps, { children: _jsx(Course, Object.assign({ courseId: course.id }, CourseComponentProps)) }), course.id))), authUserId && ((onlyStaffEnabled && canCreateCourse) || !onlyStaffEnabled) && courses.length % 2 !== 0 && (_jsx(Grid, Object.assign({ item: true, xs: 12, sm: 12, md: 6, lg: 3, className: classes.itemPlaceholder }, GridItemComponentProps, { children: _jsx(CourseCreatePlaceholder, { CreateCourseButtonComponentProps: CreateCourseButtonComponentProps }) }), "placeholder-item"))] }) })) }))) })] })); /** * Renders root object (if content availability community option is false and user is anonymous, component is hidden) */ if (!contentAvailability && !scUserContext.user) { return _jsx(HiddenPlaceholder, {}); } if (loading) { return _jsx(Skeleton, Object.assign({}, CoursesSkeletonComponentProps, { CourseSkeletonProps: CourseSkeletonComponentProps })); } return (_jsx(Root, Object.assign({ className: classNames(classes.root, className) }, rest, { children: c }))); }