invites basics

This commit is contained in:
Jeremy Kauffman 2017-08-17 23:31:44 -04:00
parent 1cbc96533b
commit 31fb723d87
18 changed files with 404 additions and 7 deletions

View file

@ -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 },
});
});
};
}

View file

@ -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);

View file

@ -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 (
<section className="card">
<div className="card__title-primary">
<h3>{__("Invite History")}</h3>
</div>
<div className="card__content">
{invitees.length === 0 &&
<span className="empty">{__("You haven't invited anyone.")} </span>}
{invitees.length > 0 &&
<table className="table-standard table-stretch">
<thead>
<tr>
<th>
{__("Invitee Email")}
</th>
<th className="text-center">
{__("Invite Status")}
</th>
<th className="text-center">
{__("Reward")}
</th>
</tr>
</thead>
<tbody>
{invitees.map((invitee, index) => {
return (
<tr key={index}>
<td>{invitee.email}</td>
<td className="text-center">
{invitee.invite_accepted && <Icon icon="icon-check" />}
</td>
<td className="text-center">
{invitee.invite_reward_claimed &&
<Icon icon="icon-check" />}
</td>
</tr>
);
})}
</tbody>
</table>}
</div>
</section>
);
}
}
export default InviteList;

View file

@ -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);

View file

@ -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 (
<form>
<FormRow
type="text"
label="Email"
placeholder="youremail@example.org"
name="email"
value={this.state.email}
errorMessage={errorMessage}
onChange={event => {
this.handleEmailChanged(event);
}}
/>
<div className="form-row-submit">
<Link
button="primary"
label={__("Send Invite")}
disabled={isPending}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div>
</form>
);
}
}
class InviteNew extends React.PureComponent {
render() {
const {
errorMessage,
invitesRemaining,
inviteNew,
inviteStatusIsPending,
isPending,
} = this.props;
return (
<section className="card">
<div className="card__title-primary">
<h3>
{__(
"Invite a Friend (or Enemy) (or Someone You Are Somewhat Ambivalent About)"
)}
</h3>
</div>
<div className="card__content">
{invitesRemaining > 0 &&
<p>{__("You have %s invites remaining.", invitesRemaining)}</p>}
{invitesRemaining <= 0 &&
<p>
<span className="empty">
{__("You have no invites.", invitesRemaining)}
</span>
</p>}
</div>
{!inviteStatusIsPending &&
invitesRemaining > 0 &&
<div className="card__content">
<FormInviteNew
errorMessage={errorMessage}
inviteNew={inviteNew}
isPending={isPending}
/>
</div>}
</section>
);
}
}
export default InviteNew;

View file

@ -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: <DiscoverPage params={params} />,
downloaded: <FileListDownloaded params={params} />,
help: <HelpPage params={params} />,
invite: <InvitePage params={params} />,
publish: <PublishPage params={params} />,
published: <FileListPublished params={params} />,
receive: <ReceiveCreditsPage params={params} />,

View file

@ -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,

View file

@ -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);

View file

@ -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

View file

@ -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 => {
<h3 className="modal__header">{__("Claim Your Credits")}</h3>
<p>
The LBRY network is controlled and powered by credits called{" "}
<em>LBC</em>, a blockchain asset.
<em><CurrencySymbol /></em>, a blockchain asset.
</p>
<p>
{__("New patrons receive ")} {" "}

View file

@ -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);

View file

@ -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 (
<main className="main--single-column">
<SubHeader />
{isPending &&
<BusyMessage message={__("Checking your invite status")} />}
{!isPending &&
isFailed &&
<span className="empty">
{__("Failed to retrieve invite status.")}
</span>}
{!isPending && !isFailed && <InviteNew />}
{!isPending && !isFailed && <InviteList />}
</main>
);
}
}
export default InvitePage;

View file

@ -7,7 +7,6 @@ const ReceiveCreditsPage = props => {
return (
<main className="main--single-column">
<SubHeader />
<WalletBalance />
<WalletAddress />
</main>
);

View file

@ -7,7 +7,6 @@ const SendCreditsPage = props => {
return (
<main className="main--single-column">
<SubHeader />
<WalletBalance />
<WalletSend />
</main>
);

View file

@ -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);

View file

@ -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":

View file

@ -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
);

View file

@ -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;