@selfcommunity/react-ui
Version:
React UI Components to integrate a Community created with SelfCommunity Platform.
234 lines (228 loc) • 15.7 kB
JavaScript
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 })));
}