diff --git a/modules/tribes/client/api/tribes.api.js b/modules/tribes/client/api/tribes.api.js new file mode 100644 index 0000000000..9040de3c0b --- /dev/null +++ b/modules/tribes/client/api/tribes.api.js @@ -0,0 +1,11 @@ +import axios from 'axios'; + +export async function join(tribeId) { + const { data } = await axios.post(`/api/users/memberships/${tribeId}`); + return data; +} + +export async function leave(tribeId) { + const { data } = await axios.delete(`/api/users/memberships/${tribeId}`); + return data; +} diff --git a/modules/tribes/client/components/JoinButton.component.js b/modules/tribes/client/components/JoinButton.component.js new file mode 100644 index 0000000000..0ed1b375a6 --- /dev/null +++ b/modules/tribes/client/components/JoinButton.component.js @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; +import LeaveTribeModal from './LeaveTribeModal'; +import { OverlayTrigger, Tooltip } from 'react-bootstrap'; + +import * as api from '../api/tribes.api'; + +function JoinButtonPresentational({ isMember, isLoading, joinLabel='Join', joinedLabel='Joined', tribe, isLoggedIn, onToggle }) { + const { t } = useTranslation('tribes'); + + const ariaLabel = (isMember) ? t('Leave Tribe') : t(`${joinLabel} ({{label}})`, { label: tribe.label }); + const buttonLabel = (isMember) ? t(joinedLabel) : t(joinLabel); + + // a button to be shown when user is signed out + if (!isLoggedIn) { + return + {buttonLabel} + ; + } + + // a button for joining and leaving a tribe + const leaveTooltip = {t('Leave Tribe')}; + const btn = ; + + if (isMember) { + return {btn}; + } else { + return btn; + } +} + +JoinButtonPresentational.propTypes = { + isMember: PropTypes.bool.isRequired, + isLoading: PropTypes.bool, + isLoggedIn: PropTypes.bool.isRequired, + joinLabel: PropTypes.string, + joinedLabel: PropTypes.string, + tribe: PropTypes.object.isRequired, + onToggle: PropTypes.func, +}; + +// @TODO this can (and should) be replaced by other container, when we finish the migration; when we start using redux etc. +export default function JoinButton({ tribe, user, onUpdated, ...rest }) { + // isLeaving controls whether the modal for leaving a tribe is shown + const [isLeaving, setIsLeaving] = useState(false); + + const [isUpdating, setIsUpdating] = useState(false); + + const isMemberInitial = user && user.memberIds && user.memberIds.indexOf(tribe._id) > -1; + const [isMember, setIsMember] = useState(isMemberInitial); + + /** + * Handle joining or leaving of a tribe + * - Join: join tribe immediately (api call) + * - Leave: show confirmation modal + */ + async function handleToggleMembership() { + if (isUpdating) { + return; + } + + if (isMember) { + setIsLeaving(true); + } else { + // updating starts + setIsUpdating(true); + + // join + const data = await api.join(tribe._id); + // update the membership locally + setIsMember(true); + + // updating finished + setIsUpdating(false); + // tell the ancestor components that the membership was updated + onUpdated(data); + } + } + + /** + * Leave a tribe (api call) + */ + async function handleLeave() { + setIsUpdating(true); + const data = await api.leave(tribe._id); + setIsUpdating(false); + setIsLeaving(false); + setIsMember(false); + // tell the ancestor components that the membership was updated + onUpdated(data); + } + + function handleCancelLeave() { + setIsLeaving(false); + } + + return <> + + + ; +} + +JoinButton.propTypes = { + tribe: PropTypes.object.isRequired, + user: PropTypes.object, + onUpdated: PropTypes.func.isRequired, +}; diff --git a/modules/tribes/client/components/LeaveTribeModal.js b/modules/tribes/client/components/LeaveTribeModal.js new file mode 100644 index 0000000000..881858d2f0 --- /dev/null +++ b/modules/tribes/client/components/LeaveTribeModal.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; +import { Modal } from 'react-bootstrap'; + +export default function LeaveTribeModal({ tribe, show, onConfirm, onCancel }) { + + const { t } = useTranslation('tribes'); + + return ( + +
+ + {t('Leave this Tribe?')} + + + + {t('Do you want to leave "{{label}}"?', { label: tribe.label })} + + + + + + +
+
+ ); +} + +LeaveTribeModal.propTypes = { + show: PropTypes.bool.isRequired, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + tribe: PropTypes.object.isRequired, +}; diff --git a/modules/tribes/client/controllers/tribes-list.client.controller.js b/modules/tribes/client/controllers/tribes-list.client.controller.js index e15e6c01e2..e2363cb2fa 100644 --- a/modules/tribes/client/controllers/tribes-list.client.controller.js +++ b/modules/tribes/client/controllers/tribes-list.client.controller.js @@ -3,7 +3,7 @@ angular .controller('TribesListController', TribesListController); /* @ngInject */ -function TribesListController(tribes, $state, Authentication, TribeService, $scope) { +function TribesListController(tribes, $state, Authentication, TribeService, $rootScope, $scope) { // ViewModel const vm = this; @@ -12,6 +12,16 @@ function TribesListController(tribes, $state, Authentication, TribeService, $sco vm.tribes = tribes; vm.user = Authentication.user; vm.openTribe = openTribe; + vm.broadcastChange = function (data) { + if (data.tribe) { + $rootScope.$broadcast('tribeUpdated', data.tribe); + } + + if (data.user) { + Authentication.user = data.user; + $rootScope.$broadcast('userUpdated'); + } + }; /** * Open tribe diff --git a/modules/tribes/client/views/tribes-list.client.view.html b/modules/tribes/client/views/tribes-list.client.view.html index 6c9cfc7934..6c262cd7de 100644 --- a/modules/tribes/client/views/tribes-list.client.view.html +++ b/modules/tribes/client/views/tribes-list.client.view.html @@ -38,11 +38,13 @@

- - + user="app.user" + icon="true" + onUpdated="tribesList.broadcastChange" + >