Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate tribe's JoinButton to React #1123

Merged
merged 7 commits into from
Dec 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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