Skip to content

Commit

Permalink
Migrate tribe's JoinButton to React (#1123)
Browse files Browse the repository at this point in the history
as seen on /tribes page
  • Loading branch information
mrkvon committed Jan 5, 2020
1 parent cf4b683 commit 60261e2
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 5 deletions.
11 changes: 11 additions & 0 deletions modules/tribes/client/api/tribes.api.js
Original file line number Diff line number Diff line change
@@ -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;
}
120 changes: 120 additions & 0 deletions modules/tribes/client/components/JoinButton.component.js
Original file line number Diff line number Diff line change
@@ -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 <a
href={`/signup?tribe=${tribe.slug}`}
type="button"
className="btn btn-sm btn-default tribe-join"
>
<i className="icon-plus" /> {buttonLabel}
</a>;
}

// a button for joining and leaving a tribe
const leaveTooltip = <Tooltip id={`tribe-${tribe._id}`} placement="bottom">{t('Leave Tribe')}</Tooltip>;
const btn = <button
type="button"
className={`${isMember ? 'btn-active' : ''} btn btn-sm btn-default tribe-join`}
onClick={onToggle}
disabled={isLoading}
aria-label={ariaLabel}
>
<i className={(isMember) ? 'icon-ok' : 'icon-plus'} /> {buttonLabel}
</button>;

if (isMember) {
return <OverlayTrigger placement="bottom" overlay={leaveTooltip}>{btn}</OverlayTrigger>;
} 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 <>
<LeaveTribeModal show={isLeaving} tribe={tribe} onConfirm={handleLeave} onCancel={handleCancelLeave}/>
<JoinButtonPresentational tribe={tribe} isLoggedIn={!!user} isMember={isMember} isLoading={isUpdating} {...rest} onToggle={handleToggleMembership} />
</>;
}

JoinButton.propTypes = {
tribe: PropTypes.object.isRequired,
user: PropTypes.object,
onUpdated: PropTypes.func.isRequired,
};
35 changes: 35 additions & 0 deletions modules/tribes/client/components/LeaveTribeModal.js
Original file line number Diff line number Diff line change
@@ -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 (
<Modal show={show} onHide={onCancel}>
<div className="modal-content">
<Modal.Header>
<Modal.Title>{t('Leave this Tribe?')}</Modal.Title>
</Modal.Header>

<Modal.Body>
{t('Do you want to leave "{{label}}"?', { label: tribe.label })}
</Modal.Body>

<Modal.Footer>
<button className="btn btn-primary" onClick={onConfirm}>{t('Leave Tribe')}</button>
<button className="btn btn-default" onClick={onCancel}>{t('Cancel')}</button>
</Modal.Footer>
</div>
</Modal>
);
}

LeaveTribeModal.propTypes = {
show: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
tribe: PropTypes.object.isRequired,
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
.controller('TribesListController', TribesListController);

/* @ngInject */
function TribesListController(tribes, $state, Authentication, TribeService, $scope) {
function TribesListController(tribes, $state, Authentication, TribeService, $rootScope, $scope) {

// ViewModel
const vm = this;
Expand All @@ -13,6 +13,16 @@
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
Expand Down
10 changes: 6 additions & 4 deletions modules/tribes/client/views/tribes-list.client.view.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ <h3 class="font-brand-light tribe-label" ng-bind="::tribe.label"></h3>
</div>
</a>
<div class="tribe-actions">
<tr-tribe-join-button
class="btn btn-sm btn-default tribe-join"
<join-button
ng-if="tribe"
tribe="tribe"
icon="true">
</tr-tribe-join-button>
user="app.user"
icon="true"
onUpdated="tribesList.broadcastChange"
></join-button>
</div>
</li>
<li>
Expand Down

0 comments on commit 60261e2

Please sign in to comment.