@selfcommunity/react-ui 1.3.0-alpha.0 → 1.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,34 @@
1
1
  import { WidgetProps } from '../Widget';
2
2
  import { SCLeaderboardEntry } from '@selfcommunity/types';
3
+ import { LeaderboardPeriod } from '@selfcommunity/utils';
4
+ export type LeaderboardPositionWidgetPeriod = LeaderboardPeriod;
3
5
  export interface LeaderboardPositionWidgetProps extends WidgetProps {
4
- position: SCLeaderboardEntry;
6
+ /**
7
+ * The leaderboard entry of the logged user. If not provided, the component fetches it on its own.
8
+ */
9
+ position?: SCLeaderboardEntry;
10
+ /**
11
+ * The period the position refers to.
12
+ * @default 'month'
13
+ */
14
+ period?: LeaderboardPositionWidgetPeriod;
15
+ /**
16
+ * If true, shows the skeleton in place of the widget content.
17
+ * @default true
18
+ */
5
19
  isLoading?: boolean;
20
+ /**
21
+ * The url the widget links to.
22
+ * @default the profile of the logged user
23
+ */
24
+ to?: string;
6
25
  }
7
26
  /**
8
27
  * > API documentation for the Community-JS Leaderboard Position Widget component. Learn about the available props and the CSS API.
9
28
  *
10
29
  *
11
30
  * This component renders a widget showing the current logged user position in the leaderboard.
31
+ * The position can be provided via the `position` prop; if it is not provided, the component fetches it on its own.
12
32
  * The widget is hidden if the user is not authenticated, the leaderboard feature is disabled or no position data is available.
13
33
 
14
34
  #### Import
@@ -10,6 +10,9 @@ const Widget_1 = tslib_1.__importDefault(require("../Widget"));
10
10
  const system_1 = require("@mui/system");
11
11
  const constants_1 = require("./constants");
12
12
  const react_core_1 = require("@selfcommunity/react-core");
13
+ const api_services_1 = require("@selfcommunity/api-services");
14
+ const utils_1 = require("@selfcommunity/utils");
15
+ const Errors_1 = require("../../constants/Errors");
13
16
  const HiddenPlaceholder_1 = tslib_1.__importDefault(require("../../shared/HiddenPlaceholder"));
14
17
  const Skeleton_1 = tslib_1.__importDefault(require("./Skeleton"));
15
18
  const classes = {
@@ -31,6 +34,7 @@ const Root = (0, material_1.styled)(Widget_1.default, {
31
34
  *
32
35
  *
33
36
  * This component renders a widget showing the current logged user position in the leaderboard.
37
+ * The position can be provided via the `position` prop; if it is not provided, the component fetches it on its own.
34
38
  * The widget is hidden if the user is not authenticated, the leaderboard feature is disabled or no position data is available.
35
39
 
36
40
  #### Import
@@ -64,12 +68,38 @@ function LeaderboardPositionWidget(inProps) {
64
68
  props: inProps,
65
69
  name: constants_1.PREFIX
66
70
  });
67
- const { className, position, isLoading = true } = props, rest = tslib_1.__rest(props, ["className", "position", "isLoading"]);
71
+ const { className, position: positionProp, period = 'month', isLoading: isLoadingProp = true, to } = props, rest = tslib_1.__rest(props, ["className", "position", "period", "isLoading", "to"]);
68
72
  // CONTEXT
69
73
  const scUserContext = (0, react_1.useContext)(react_core_1.SCUserContext);
70
74
  const scRoutingContext = (0, react_core_1.useSCRouting)();
71
75
  // PREFERENCES
72
76
  const leaderboardsEnabled = (0, react_core_1.useSCPreferenceEnabled)(react_core_1.SCPreferences.CONFIGURATIONS_LEADERBOARDS_ENABLED);
77
+ // Self-fetches the logged user position when it is not provided by the parent
78
+ const shouldFetch = Boolean(scUserContext.user) && leaderboardsEnabled && !positionProp;
79
+ const [fetchedPosition, setFetchedPosition] = (0, react_1.useState)(null);
80
+ const [isFetching, setIsFetching] = (0, react_1.useState)(shouldFetch);
81
+ const isMountedRef = (0, react_core_1.useIsComponentMountedRef)();
82
+ (0, react_1.useEffect)(() => {
83
+ if (!shouldFetch) {
84
+ return;
85
+ }
86
+ setIsFetching(true);
87
+ api_services_1.ScoreService.getLeaderboards(Object.assign({ limit: 1 }, (0, utils_1.getPeriodRange)(period)))
88
+ .then((data) => {
89
+ if (isMountedRef.current) {
90
+ setFetchedPosition(data.my_position);
91
+ setIsFetching(false);
92
+ }
93
+ })
94
+ .catch((error) => {
95
+ utils_1.Logger.error(Errors_1.SCOPE_SC_UI, error);
96
+ if (isMountedRef.current) {
97
+ setIsFetching(false);
98
+ }
99
+ });
100
+ }, [shouldFetch, period]);
101
+ const position = shouldFetch ? fetchedPosition : positionProp;
102
+ const isLoading = shouldFetch ? isFetching : isLoadingProp;
73
103
  /**
74
104
  * Renders nothing if the user is not authenticated or the leaderboard feature is disabled
75
105
  */
@@ -88,6 +118,6 @@ function LeaderboardPositionWidget(inProps) {
88
118
  if (!position) {
89
119
  return (0, jsx_runtime_1.jsx)(HiddenPlaceholder_1.default, {});
90
120
  }
91
- return ((0, jsx_runtime_1.jsx)(Root, Object.assign({ className: (0, classnames_1.default)(classes.root, className) }, rest, { children: (0, jsx_runtime_1.jsxs)(material_1.CardContent, { children: [(0, jsx_runtime_1.jsx)(material_1.Typography, Object.assign({ variant: "h4", className: classes.title }, { children: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.leaderboardPositionWidget.title", defaultMessage: "ui.leaderboardPositionWidget.title" }) })), (0, jsx_runtime_1.jsxs)(material_1.Stack, Object.assign({ direction: "row", spacing: 1.5, alignItems: "center", component: react_core_1.Link, to: scRoutingContext.url(react_core_1.SCRoutes.USER_PROFILE_ROUTE_NAME, { id: position.user.id }), className: classes.user }, { children: [(0, jsx_runtime_1.jsx)(material_1.Avatar, { alt: position.user.username, src: position.user.avatar, className: classes.userAvatar }), (0, jsx_runtime_1.jsx)(material_1.Typography, Object.assign({ variant: "body1", fontWeight: 600, className: classes.userName }, { children: position.user.real_name || position.user.username }))] })), (0, jsx_runtime_1.jsxs)(material_1.Stack, Object.assign({ direction: "row", justifyContent: "space-between", alignItems: "center", className: classes.footer }, { children: [(0, jsx_runtime_1.jsx)(material_1.Typography, Object.assign({ variant: "h3", className: classes.position }, { children: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.leaderboardPositionWidget.position", defaultMessage: "ui.leaderboardPositionWidget.position", values: { position: position.position } }) })), (0, jsx_runtime_1.jsx)(material_1.Typography, Object.assign({ variant: "body1", className: classes.score }, { children: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.leaderboardPositionWidget.score", defaultMessage: "ui.leaderboardPositionWidget.score", values: { score: position.total_score } }) }))] }))] }) })));
121
+ return ((0, jsx_runtime_1.jsx)(Root, Object.assign({ className: (0, classnames_1.default)(classes.root, className) }, rest, { children: (0, jsx_runtime_1.jsxs)(material_1.CardContent, { children: [(0, jsx_runtime_1.jsx)(material_1.Typography, Object.assign({ variant: "h4", className: classes.title }, { children: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.leaderboardPositionWidget.title", defaultMessage: "ui.leaderboardPositionWidget.title" }) })), (0, jsx_runtime_1.jsxs)(material_1.Stack, Object.assign({ direction: "row", spacing: 1.5, alignItems: "center", component: react_core_1.Link, to: to !== null && to !== void 0 ? to : scRoutingContext.url(react_core_1.SCRoutes.USER_PROFILE_ROUTE_NAME, { id: position.user.id }), className: classes.user }, { children: [(0, jsx_runtime_1.jsx)(material_1.Avatar, { alt: position.user.username, src: position.user.avatar, className: classes.userAvatar }), (0, jsx_runtime_1.jsx)(material_1.Typography, Object.assign({ variant: "body1", fontWeight: 600, className: classes.userName }, { children: position.user.real_name || position.user.username }))] })), (0, jsx_runtime_1.jsxs)(material_1.Stack, Object.assign({ direction: "row", justifyContent: "space-between", alignItems: "center", className: classes.footer }, { children: [(0, jsx_runtime_1.jsx)(material_1.Typography, Object.assign({ variant: "h3", className: classes.position }, { children: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.leaderboardPositionWidget.position", defaultMessage: "ui.leaderboardPositionWidget.position", values: { position: position.position } }) })), (0, jsx_runtime_1.jsx)(material_1.Typography, Object.assign({ variant: "body1", className: classes.score }, { children: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.leaderboardPositionWidget.score", defaultMessage: "ui.leaderboardPositionWidget.score", values: { score: position.total_score } }) }))] }))] }) })));
92
122
  }
93
123
  exports.default = LeaderboardPositionWidget;
@@ -17,6 +17,7 @@ const Errors_1 = require("../../constants/Errors");
17
17
  const HiddenPlaceholder_1 = tslib_1.__importDefault(require("../../shared/HiddenPlaceholder"));
18
18
  const Skeleton_1 = tslib_1.__importDefault(require("./Skeleton"));
19
19
  const RANKING_LIMIT = 5;
20
+ const PODIUM_LIMIT = 3;
20
21
  const classes = {
21
22
  root: `${constants_1.PREFIX}-root`,
22
23
  podiumHeader: `${constants_1.PREFIX}-podium-header`,
@@ -140,7 +141,10 @@ function LeaderboardWidget(inProps) {
140
141
  if (isLoading) {
141
142
  return (0, jsx_runtime_1.jsx)(Skeleton_1.default, { mode: mode });
142
143
  }
143
- const podium = entries.filter((entry) => entry.position <= 3);
144
+ // Entries can share the same position: the podium shows only the first one of each of the first three positions, while the ranking list below shows them all
145
+ const podium = entries
146
+ .filter((entry, index) => entry.position <= PODIUM_LIMIT && entries.findIndex((e) => e.position === entry.position) === index)
147
+ .slice(0, PODIUM_LIMIT);
144
148
  /**
145
149
  * Renders nothing if there's no podium data
146
150
  */
@@ -1,14 +1,34 @@
1
1
  import { WidgetProps } from '../Widget';
2
2
  import { SCLeaderboardEntry } from '@selfcommunity/types';
3
+ import { LeaderboardPeriod } from '@selfcommunity/utils';
4
+ export type LeaderboardPositionWidgetPeriod = LeaderboardPeriod;
3
5
  export interface LeaderboardPositionWidgetProps extends WidgetProps {
4
- position: SCLeaderboardEntry;
6
+ /**
7
+ * The leaderboard entry of the logged user. If not provided, the component fetches it on its own.
8
+ */
9
+ position?: SCLeaderboardEntry;
10
+ /**
11
+ * The period the position refers to.
12
+ * @default 'month'
13
+ */
14
+ period?: LeaderboardPositionWidgetPeriod;
15
+ /**
16
+ * If true, shows the skeleton in place of the widget content.
17
+ * @default true
18
+ */
5
19
  isLoading?: boolean;
20
+ /**
21
+ * The url the widget links to.
22
+ * @default the profile of the logged user
23
+ */
24
+ to?: string;
6
25
  }
7
26
  /**
8
27
  * > API documentation for the Community-JS Leaderboard Position Widget component. Learn about the available props and the CSS API.
9
28
  *
10
29
  *
11
30
  * This component renders a widget showing the current logged user position in the leaderboard.
31
+ * The position can be provided via the `position` prop; if it is not provided, the component fetches it on its own.
12
32
  * The widget is hidden if the user is not authenticated, the leaderboard feature is disabled or no position data is available.
13
33
 
14
34
  #### Import
@@ -1,13 +1,16 @@
1
1
  import { __rest } from "tslib";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useContext } from 'react';
3
+ import { useContext, useEffect, useState } from 'react';
4
4
  import { Avatar, CardContent, Stack, Typography, styled } from '@mui/material';
5
5
  import { FormattedMessage } from 'react-intl';
6
6
  import classNames from 'classnames';
7
7
  import Widget from '../Widget';
8
8
  import { useThemeProps } from '@mui/system';
9
9
  import { PREFIX } from './constants';
10
- import { Link, SCPreferences, SCRoutes, SCUserContext, useSCPreferenceEnabled, useSCRouting } from '@selfcommunity/react-core';
10
+ import { Link, SCPreferences, SCRoutes, SCUserContext, useIsComponentMountedRef, useSCPreferenceEnabled, useSCRouting } from '@selfcommunity/react-core';
11
+ import { ScoreService } from '@selfcommunity/api-services';
12
+ import { getPeriodRange, Logger } from '@selfcommunity/utils';
13
+ import { SCOPE_SC_UI } from '../../constants/Errors';
11
14
  import HiddenPlaceholder from '../../shared/HiddenPlaceholder';
12
15
  import LeaderboardPositionWidgetSkeleton from './Skeleton';
13
16
  const classes = {
@@ -29,6 +32,7 @@ const Root = styled(Widget, {
29
32
  *
30
33
  *
31
34
  * This component renders a widget showing the current logged user position in the leaderboard.
35
+ * The position can be provided via the `position` prop; if it is not provided, the component fetches it on its own.
32
36
  * The widget is hidden if the user is not authenticated, the leaderboard feature is disabled or no position data is available.
33
37
 
34
38
  #### Import
@@ -62,12 +66,38 @@ export default function LeaderboardPositionWidget(inProps) {
62
66
  props: inProps,
63
67
  name: PREFIX
64
68
  });
65
- const { className, position, isLoading = true } = props, rest = __rest(props, ["className", "position", "isLoading"]);
69
+ const { className, position: positionProp, period = 'month', isLoading: isLoadingProp = true, to } = props, rest = __rest(props, ["className", "position", "period", "isLoading", "to"]);
66
70
  // CONTEXT
67
71
  const scUserContext = useContext(SCUserContext);
68
72
  const scRoutingContext = useSCRouting();
69
73
  // PREFERENCES
70
74
  const leaderboardsEnabled = useSCPreferenceEnabled(SCPreferences.CONFIGURATIONS_LEADERBOARDS_ENABLED);
75
+ // Self-fetches the logged user position when it is not provided by the parent
76
+ const shouldFetch = Boolean(scUserContext.user) && leaderboardsEnabled && !positionProp;
77
+ const [fetchedPosition, setFetchedPosition] = useState(null);
78
+ const [isFetching, setIsFetching] = useState(shouldFetch);
79
+ const isMountedRef = useIsComponentMountedRef();
80
+ useEffect(() => {
81
+ if (!shouldFetch) {
82
+ return;
83
+ }
84
+ setIsFetching(true);
85
+ ScoreService.getLeaderboards(Object.assign({ limit: 1 }, getPeriodRange(period)))
86
+ .then((data) => {
87
+ if (isMountedRef.current) {
88
+ setFetchedPosition(data.my_position);
89
+ setIsFetching(false);
90
+ }
91
+ })
92
+ .catch((error) => {
93
+ Logger.error(SCOPE_SC_UI, error);
94
+ if (isMountedRef.current) {
95
+ setIsFetching(false);
96
+ }
97
+ });
98
+ }, [shouldFetch, period]);
99
+ const position = shouldFetch ? fetchedPosition : positionProp;
100
+ const isLoading = shouldFetch ? isFetching : isLoadingProp;
71
101
  /**
72
102
  * Renders nothing if the user is not authenticated or the leaderboard feature is disabled
73
103
  */
@@ -86,5 +116,5 @@ export default function LeaderboardPositionWidget(inProps) {
86
116
  if (!position) {
87
117
  return _jsx(HiddenPlaceholder, {});
88
118
  }
89
- return (_jsx(Root, Object.assign({ className: classNames(classes.root, className) }, rest, { children: _jsxs(CardContent, { children: [_jsx(Typography, Object.assign({ variant: "h4", className: classes.title }, { children: _jsx(FormattedMessage, { id: "ui.leaderboardPositionWidget.title", defaultMessage: "ui.leaderboardPositionWidget.title" }) })), _jsxs(Stack, Object.assign({ direction: "row", spacing: 1.5, alignItems: "center", component: Link, to: scRoutingContext.url(SCRoutes.USER_PROFILE_ROUTE_NAME, { id: position.user.id }), className: classes.user }, { children: [_jsx(Avatar, { alt: position.user.username, src: position.user.avatar, className: classes.userAvatar }), _jsx(Typography, Object.assign({ variant: "body1", fontWeight: 600, className: classes.userName }, { children: position.user.real_name || position.user.username }))] })), _jsxs(Stack, Object.assign({ direction: "row", justifyContent: "space-between", alignItems: "center", className: classes.footer }, { children: [_jsx(Typography, Object.assign({ variant: "h3", className: classes.position }, { children: _jsx(FormattedMessage, { id: "ui.leaderboardPositionWidget.position", defaultMessage: "ui.leaderboardPositionWidget.position", values: { position: position.position } }) })), _jsx(Typography, Object.assign({ variant: "body1", className: classes.score }, { children: _jsx(FormattedMessage, { id: "ui.leaderboardPositionWidget.score", defaultMessage: "ui.leaderboardPositionWidget.score", values: { score: position.total_score } }) }))] }))] }) })));
119
+ return (_jsx(Root, Object.assign({ className: classNames(classes.root, className) }, rest, { children: _jsxs(CardContent, { children: [_jsx(Typography, Object.assign({ variant: "h4", className: classes.title }, { children: _jsx(FormattedMessage, { id: "ui.leaderboardPositionWidget.title", defaultMessage: "ui.leaderboardPositionWidget.title" }) })), _jsxs(Stack, Object.assign({ direction: "row", spacing: 1.5, alignItems: "center", component: Link, to: to !== null && to !== void 0 ? to : scRoutingContext.url(SCRoutes.USER_PROFILE_ROUTE_NAME, { id: position.user.id }), className: classes.user }, { children: [_jsx(Avatar, { alt: position.user.username, src: position.user.avatar, className: classes.userAvatar }), _jsx(Typography, Object.assign({ variant: "body1", fontWeight: 600, className: classes.userName }, { children: position.user.real_name || position.user.username }))] })), _jsxs(Stack, Object.assign({ direction: "row", justifyContent: "space-between", alignItems: "center", className: classes.footer }, { children: [_jsx(Typography, Object.assign({ variant: "h3", className: classes.position }, { children: _jsx(FormattedMessage, { id: "ui.leaderboardPositionWidget.position", defaultMessage: "ui.leaderboardPositionWidget.position", values: { position: position.position } }) })), _jsx(Typography, Object.assign({ variant: "body1", className: classes.score }, { children: _jsx(FormattedMessage, { id: "ui.leaderboardPositionWidget.score", defaultMessage: "ui.leaderboardPositionWidget.score", values: { score: position.total_score } }) }))] }))] }) })));
90
120
  }
@@ -15,6 +15,7 @@ import { SCOPE_SC_UI } from '../../constants/Errors';
15
15
  import HiddenPlaceholder from '../../shared/HiddenPlaceholder';
16
16
  import LeaderboardWidgetSkeleton from './Skeleton';
17
17
  const RANKING_LIMIT = 5;
18
+ const PODIUM_LIMIT = 3;
18
19
  const classes = {
19
20
  root: `${PREFIX}-root`,
20
21
  podiumHeader: `${PREFIX}-podium-header`,
@@ -138,7 +139,10 @@ export default function LeaderboardWidget(inProps) {
138
139
  if (isLoading) {
139
140
  return _jsx(LeaderboardWidgetSkeleton, { mode: mode });
140
141
  }
141
- const podium = entries.filter((entry) => entry.position <= 3);
142
+ // Entries can share the same position: the podium shows only the first one of each of the first three positions, while the ranking list below shows them all
143
+ const podium = entries
144
+ .filter((entry, index) => entry.position <= PODIUM_LIMIT && entries.findIndex((e) => e.position === entry.position) === index)
145
+ .slice(0, PODIUM_LIMIT);
142
146
  /**
143
147
  * Renders nothing if there's no podium data
144
148
  */