From 31fb723d878131425d78928e7fb28699750b8035 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 17 Aug 2017 23:31:44 -0400 Subject: [PATCH] invites basics --- ui/js/actions/user.js | 59 +++++++++++++++ ui/js/component/inviteList/index.js | 16 ++++ ui/js/component/inviteList/view.jsx | 58 ++++++++++++++ ui/js/component/inviteNew/index.js | 21 ++++++ ui/js/component/inviteNew/view.jsx | 100 +++++++++++++++++++++++++ ui/js/component/router/view.jsx | 2 + ui/js/component/userEmailNew/index.js | 2 +- ui/js/component/walletAddress/index.js | 4 +- ui/js/constants/action_types.js | 9 +++ ui/js/modal/modalCreditIntro/view.jsx | 4 +- ui/js/page/invite/index.js | 19 +++++ ui/js/page/invite/view.jsx | 32 ++++++++ ui/js/page/receiveCredits/view.jsx | 1 - ui/js/page/sendCredits/view.jsx | 1 - ui/js/reducers/user.js | 48 ++++++++++++ ui/js/selectors/app.js | 2 + ui/js/selectors/user.js | 30 ++++++++ ui/scss/component/_table.scss | 3 + 18 files changed, 404 insertions(+), 7 deletions(-) create mode 100644 ui/js/component/inviteList/index.js create mode 100644 ui/js/component/inviteList/view.jsx create mode 100644 ui/js/component/inviteNew/index.js create mode 100644 ui/js/component/inviteNew/view.jsx create mode 100644 ui/js/page/invite/index.js create mode 100644 ui/js/page/invite/view.jsx diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js index 8a0e59ef1..e649bed88 100644 --- a/ui/js/actions/user.js +++ b/ui/js/actions/user.js @@ -2,6 +2,7 @@ import * as types from "constants/action_types"; import * as modals from "constants/modal_types"; import lbryio from "lbryio"; import { doOpenModal } from "actions/app"; +import { doOpenModal, doShowSnackBar } from "actions/app"; import { doRewardList, doClaimRewardType } from "actions/rewards"; import { selectEmailToVerify, selectUser } from "selectors/user"; import rewards from "rewards"; @@ -172,3 +173,61 @@ export function doFetchAccessToken() { lbryio.getAuthToken().then(success); }; } + +export function doFetchInviteStatus() { + return function(dispatch, getState) { + dispatch({ + type: types.USER_INVITE_STATUS_FETCH_STARTED, + }); + + lbryio + .call("user", "invite_status") + .then(status => { + console.log(status); + dispatch({ + type: types.USER_INVITE_STATUS_FETCH_SUCCESS, + data: { + invitesRemaining: status.invites_remaining, + invitees: status.invitees, + }, + }); + }) + .catch(error => { + dispatch({ + type: types.USER_INVITE_STATUS_FETCH_FAILURE, + data: { error }, + }); + }); + }; +} + +export function doUserInviteNew(email) { + return function(dispatch, getState) { + dispatch({ + type: types.USER_INVITE_NEW_STARTED, + }); + + lbryio + .call("user", "invite", { email: email }, "post") + .then(invite => { + dispatch({ + type: types.USER_INVITE_NEW_SUCCESS, + data: { email }, + }); + + dispatch( + doShowSnackBar({ + message: __("Invite sent to %s", email), + }) + ); + + dispatch(doFetchInviteStatus()); + }) + .catch(error => { + dispatch({ + type: types.USER_INVITE_NEW_FAILURE, + data: { error }, + }); + }); + }; +} diff --git a/ui/js/component/inviteList/index.js b/ui/js/component/inviteList/index.js new file mode 100644 index 000000000..3f7361454 --- /dev/null +++ b/ui/js/component/inviteList/index.js @@ -0,0 +1,16 @@ +import React from "react"; +import { connect } from "react-redux"; +import { + selectUserInvitees, + selectUserInviteStatusIsPending, +} from "selectors/user"; +import InviteList from "./view"; + +const select = state => ({ + invitees: selectUserInvitees(state), + isPending: selectUserInviteStatusIsPending(state), +}); + +const perform = dispatch => ({}); + +export default connect(select, perform)(InviteList); diff --git a/ui/js/component/inviteList/view.jsx b/ui/js/component/inviteList/view.jsx new file mode 100644 index 000000000..8d3d4962d --- /dev/null +++ b/ui/js/component/inviteList/view.jsx @@ -0,0 +1,58 @@ +import React from "react"; +import { Icon } from "component/common"; + +class InviteList extends React.PureComponent { + render() { + const { invitees } = this.props; + + if (!invitees) { + return null; + } + + return ( +
+
+

{__("Invite History")}

+
+
+ {invitees.length === 0 && + {__("You haven't invited anyone.")} } + {invitees.length > 0 && + + + + + + + + + + {invitees.map((invitee, index) => { + return ( + + + + + + ); + })} + +
+ {__("Invitee Email")} + + {__("Invite Status")} + + {__("Reward")} +
{invitee.email} + {invitee.invite_accepted && } + + {invitee.invite_reward_claimed && + } +
} +
+
+ ); + } +} + +export default InviteList; diff --git a/ui/js/component/inviteNew/index.js b/ui/js/component/inviteNew/index.js new file mode 100644 index 000000000..b210aaf36 --- /dev/null +++ b/ui/js/component/inviteNew/index.js @@ -0,0 +1,21 @@ +import React from "react"; +import { connect } from "react-redux"; +import InviteNew from "./view"; +import { + selectUserInvitesRemaining, + selectUserInviteNewIsPending, + selectUserInviteNewErrorMessage, +} from "selectors/user"; +import { doUserInviteNew } from "actions/user"; + +const select = state => ({ + errorMessage: selectUserInviteNewErrorMessage(state), + invitesRemaining: selectUserInvitesRemaining(state), + isPending: selectUserInviteNewIsPending(state), +}); + +const perform = dispatch => ({ + inviteNew: email => dispatch(doUserInviteNew(email)), +}); + +export default connect(select, perform)(InviteNew); diff --git a/ui/js/component/inviteNew/view.jsx b/ui/js/component/inviteNew/view.jsx new file mode 100644 index 000000000..a02f0054b --- /dev/null +++ b/ui/js/component/inviteNew/view.jsx @@ -0,0 +1,100 @@ +import React from "react"; +import { BusyMessage } from "component/common"; +import Link from "component/link"; +import { FormRow } from "component/form.js"; + +class FormInviteNew extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + email: "", + }; + } + + handleEmailChanged(event) { + this.setState({ + email: event.target.value, + }); + } + + handleSubmit(event) { + event.preventDefault(); + this.props.inviteNew(this.state.email); + } + + render() { + const { errorMessage, isPending } = this.props; + + return ( +
+ { + this.handleEmailChanged(event); + }} + /> +
+ { + this.handleSubmit(event); + }} + /> +
+ + ); + } +} + +class InviteNew extends React.PureComponent { + render() { + const { + errorMessage, + invitesRemaining, + inviteNew, + inviteStatusIsPending, + isPending, + } = this.props; + + return ( +
+
+

+ {__( + "Invite a Friend (or Enemy) (or Someone You Are Somewhat Ambivalent About)" + )} +

+
+
+ {invitesRemaining > 0 && +

{__("You have %s invites remaining.", invitesRemaining)}

} + {invitesRemaining <= 0 && +

+ + {__("You have no invites.", invitesRemaining)} + +

} +
+ {!inviteStatusIsPending && + invitesRemaining > 0 && +
+ +
} +
+ ); + } +} + +export default InviteNew; diff --git a/ui/js/component/router/view.jsx b/ui/js/component/router/view.jsx index 4b1b20641..501b521a5 100644 --- a/ui/js/component/router/view.jsx +++ b/ui/js/component/router/view.jsx @@ -16,6 +16,7 @@ import FileListPublished from "page/fileListPublished"; import ChannelPage from "page/channel"; import SearchPage from "page/search"; import AuthPage from "page/auth"; +import InvitePage from "page/invite"; import BackupPage from "page/backup"; const route = (page, routesMap) => { @@ -35,6 +36,7 @@ const Router = props => { discover: , downloaded: , help: , + invite: , publish: , published: , receive: , diff --git a/ui/js/component/userEmailNew/index.js b/ui/js/component/userEmailNew/index.js index 4040606eb..699f794ec 100644 --- a/ui/js/component/userEmailNew/index.js +++ b/ui/js/component/userEmailNew/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doUserEmailNew } from "actions/user"; +import { doUserEmailNew, doUserInviteNew } from "actions/user"; import { selectEmailNewIsPending, selectEmailNewErrorMessage, diff --git a/ui/js/component/walletAddress/index.js b/ui/js/component/walletAddress/index.js index ad0021f41..d5a8d7696 100644 --- a/ui/js/component/walletAddress/index.js +++ b/ui/js/component/walletAddress/index.js @@ -5,7 +5,7 @@ import { selectReceiveAddress, selectGettingNewAddress, } from "selectors/wallet"; -import WalletPage from "./view"; +import WalletAddress from "./view"; const select = state => ({ receiveAddress: selectReceiveAddress(state), @@ -17,4 +17,4 @@ const perform = dispatch => ({ getNewAddress: () => dispatch(doGetNewAddress()), }); -export default connect(select, perform)(WalletPage); +export default connect(select, perform)(WalletAddress); diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index e44102c54..edb60621f 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -108,6 +108,15 @@ export const USER_IDENTITY_VERIFY_FAILURE = "USER_IDENTITY_VERIFY_FAILURE"; export const USER_FETCH_STARTED = "USER_FETCH_STARTED"; export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS"; export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE"; +export const USER_INVITE_STATUS_FETCH_STARTED = + "USER_INVITE_STATUS_FETCH_STARTED"; +export const USER_INVITE_STATUS_FETCH_SUCCESS = + "USER_INVITE_STATUS_FETCH_SUCCESS"; +export const USER_INVITE_STATUS_FETCH_FAILURE = + "USER_INVITE_STATUS_FETCH_FAILURE"; +export const USER_INVITE_NEW_STARTED = "USER_INVITE_NEW_STARTED"; +export const USER_INVITE_NEW_SUCCESS = "USER_INVITE_NEW_SUCCESS"; +export const USER_INVITE_NEW_FAILURE = "USER_INVITE_NEW_FAILURE"; export const FETCH_ACCESS_TOKEN_SUCCESS = "FETCH_ACCESS_TOKEN_SUCCESS"; // Rewards diff --git a/ui/js/modal/modalCreditIntro/view.jsx b/ui/js/modal/modalCreditIntro/view.jsx index 8a2b7b500..226ab0f2e 100644 --- a/ui/js/modal/modalCreditIntro/view.jsx +++ b/ui/js/modal/modalCreditIntro/view.jsx @@ -1,6 +1,6 @@ import React from "react"; import { Modal } from "modal/modal"; -import { CreditAmount } from "component/common"; +import { CreditAmount, CurrencySymbol } from "component/common"; import Link from "component/link/index"; const ModalCreditIntro = props => { @@ -14,7 +14,7 @@ const ModalCreditIntro = props => {

{__("Claim Your Credits")}

The LBRY network is controlled and powered by credits called{" "} - LBC, a blockchain asset. + , a blockchain asset.

{__("New patrons receive ")} {" "} diff --git a/ui/js/page/invite/index.js b/ui/js/page/invite/index.js new file mode 100644 index 000000000..c97cd1b29 --- /dev/null +++ b/ui/js/page/invite/index.js @@ -0,0 +1,19 @@ +import React from "react"; +import { connect } from "react-redux"; +import InvitePage from "./view"; +import { doFetchInviteStatus } from "actions/user"; +import { + selectUserInviteStatusFailed, + selectUserInviteStatusIsPending, +} from "selectors/user"; + +const select = state => ({ + isFailed: selectUserInviteStatusFailed(state), + isPending: selectUserInviteStatusIsPending(state), +}); + +const perform = dispatch => ({ + fetchInviteStatus: () => dispatch(doFetchInviteStatus()), +}); + +export default connect(select, perform)(InvitePage); diff --git a/ui/js/page/invite/view.jsx b/ui/js/page/invite/view.jsx new file mode 100644 index 000000000..0488ca1f3 --- /dev/null +++ b/ui/js/page/invite/view.jsx @@ -0,0 +1,32 @@ +import React from "react"; +import { BusyMessage } from "component/common"; +import SubHeader from "component/subHeader"; +import InviteNew from "component/inviteNew"; +import InviteList from "component/inviteList"; + +class InvitePage extends React.PureComponent { + componentWillMount() { + this.props.fetchInviteStatus(); + } + + render() { + const { isPending, isFailed } = this.props; + + return ( +

+ + {isPending && + } + {!isPending && + isFailed && + + {__("Failed to retrieve invite status.")} + } + {!isPending && !isFailed && } + {!isPending && !isFailed && } +
+ ); + } +} + +export default InvitePage; diff --git a/ui/js/page/receiveCredits/view.jsx b/ui/js/page/receiveCredits/view.jsx index 11e0e0690..b00f8dc34 100644 --- a/ui/js/page/receiveCredits/view.jsx +++ b/ui/js/page/receiveCredits/view.jsx @@ -7,7 +7,6 @@ const ReceiveCreditsPage = props => { return (
-
); diff --git a/ui/js/page/sendCredits/view.jsx b/ui/js/page/sendCredits/view.jsx index 06e8ed5ce..b0603b9c7 100644 --- a/ui/js/page/sendCredits/view.jsx +++ b/ui/js/page/sendCredits/view.jsx @@ -7,7 +7,6 @@ const SendCreditsPage = props => { return (
-
); diff --git a/ui/js/reducers/user.js b/ui/js/reducers/user.js index 02203cdfe..bc19f3464 100644 --- a/ui/js/reducers/user.js +++ b/ui/js/reducers/user.js @@ -8,6 +8,11 @@ const defaultState = { emailNewIsPending: false, emailNewErrorMessage: "", emailToVerify: "", + inviteNewErrorMessage: "", + inviteNewIsPending: false, + inviteStatusIsPending: false, + invitesRemaining: undefined, + invitees: undefined, user: undefined, }; @@ -142,6 +147,49 @@ reducers[types.FETCH_ACCESS_TOKEN_SUCCESS] = function(state, action) { }); }; +reducers[types.USER_INVITE_STATUS_FETCH_STARTED] = function(state, action) { + return Object.assign({}, state, { + inviteStatusIsPending: true, + }); +}; + +reducers[types.USER_INVITE_STATUS_FETCH_SUCCESS] = function(state, action) { + return Object.assign({}, state, { + inviteStatusIsPending: false, + invitesRemaining: action.data.invitesRemaining, + invitees: action.data.invitees, + }); +}; + +reducers[types.USER_INVITE_NEW_STARTED] = function(state, action) { + return Object.assign({}, state, { + inviteNewIsPending: true, + inviteNewErrorMessage: "", + }); +}; + +reducers[types.USER_INVITE_NEW_SUCCESS] = function(state, action) { + return Object.assign({}, state, { + inviteNewIsPending: false, + inviteNewErrorMessage: "", + }); +}; + +reducers[types.USER_INVITE_NEW_FAILURE] = function(state, action) { + return Object.assign({}, state, { + inviteNewIsPending: false, + inviteNewErrorMessage: action.data.error.message, + }); +}; + +reducers[types.USER_INVITE_STATUS_FETCH_FAILURE] = function(state, action) { + return Object.assign({}, state, { + inviteStatusIsPending: false, + invitesRemaining: null, + invitees: null, + }); +}; + export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; if (handler) return handler(state, action); diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js index 1ad6a81b9..959c80c99 100644 --- a/ui/js/selectors/app.js +++ b/ui/js/selectors/app.js @@ -138,12 +138,14 @@ export const selectHeaderLinks = createSelector(selectCurrentPage, page => { case "wallet": case "send": case "receive": + case "invite": case "rewards": case "backup": return { wallet: __("Overview"), send: __("Send"), receive: __("Receive"), + invite: __("Invites"), rewards: __("Rewards"), }; case "downloaded": diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js index a499e5666..43ac07413 100644 --- a/ui/js/selectors/user.js +++ b/ui/js/selectors/user.js @@ -68,3 +68,33 @@ export const selectAccessToken = createSelector( _selectState, state => state.accessToken ); + +export const selectUserInviteStatusIsPending = createSelector( + _selectState, + state => state.inviteStatusIsPending +); + +export const selectUserInvitesRemaining = createSelector( + _selectState, + state => state.invitesRemaining +); + +export const selectUserInvitees = createSelector( + _selectState, + state => state.invitees +); + +export const selectUserInviteStatusFailed = createSelector( + selectUserInvitesRemaining, + inviteStatus => selectUserInvitesRemaining === null +); + +export const selectUserInviteNewIsPending = createSelector( + _selectState, + state => state.inviteNewIsPending +); + +export const selectUserInviteNewErrorMessage = createSelector( + _selectState, + state => state.inviteNewErrorMessage +); diff --git a/ui/scss/component/_table.scss b/ui/scss/component/_table.scss index 9d60cf6e8..7f5600aa1 100644 --- a/ui/scss/component/_table.scss +++ b/ui/scss/component/_table.scss @@ -24,6 +24,9 @@ table.table-standard { img { vertical-align: text-bottom; } + &.text-center { + text-align: center; + } } tr.thead:not(:first-child) th { border-top: 1px solid #e2e2e2;