diff --git a/ui/component/channelSelector/index.js b/ui/component/channelSelector/index.js new file mode 100644 index 000000000..9652ddd95 --- /dev/null +++ b/ui/component/channelSelector/index.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import SelectChannel from './view'; +import { + selectBalance, + selectMyChannelClaims, + selectFetchingMyChannels, + doFetchChannelListMine, + doCreateChannel, +} from 'lbry-redux'; +import { selectUserVerifiedEmail } from 'lbryinc'; + +const select = state => ({ + channels: selectMyChannelClaims(state), + fetchingChannels: selectFetchingMyChannels(state), + balance: selectBalance(state), + emailVerified: selectUserVerifiedEmail(state), +}); + +const perform = dispatch => ({ + createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)), + fetchChannelListMine: () => dispatch(doFetchChannelListMine()), +}); + +export default connect(select, perform)(SelectChannel); diff --git a/ui/component/channelSelector/view.jsx b/ui/component/channelSelector/view.jsx new file mode 100644 index 000000000..fa6303cca --- /dev/null +++ b/ui/component/channelSelector/view.jsx @@ -0,0 +1,59 @@ +// @flow +import * as ICONS from 'constants/icons'; +import classnames from 'classnames'; +import React from 'react'; +import ChannelThumbnail from 'component/channelThumbnail'; +import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button'; +import ClaimPreviewTitle from 'component/claimPreviewTitle'; +import Icon from 'component/common/icon'; + +type Props = { + selectedChannelUrl: string, // currently selected channel + channels: ?Array, + onChannelSelect: (url: string) => void, +}; + +type ListItemProps = { + url: string, + isSelected?: boolean, +}; + +function ChannelListItem(props: ListItemProps) { + const { url, isSelected = false } = props; + + return ( +
+ + + {isSelected && } +
+ ); +} + +function ChannelSelector(props: Props) { + const { channels, selectedChannelUrl, onChannelSelect } = props; + + if (!selectedChannelUrl) { + return null; + } + + return ( +
+ + + + + + {channels && + channels.map(channel => ( + onChannelSelect(channel.canonical_url)}> + + + ))} + + +
+ ); +} + +export default ChannelSelector; diff --git a/ui/component/common/icon.jsx b/ui/component/common/icon.jsx index 7a7eee3f7..90931ebd7 100644 --- a/ui/component/common/icon.jsx +++ b/ui/component/common/icon.jsx @@ -74,7 +74,7 @@ class IconComponent extends React.PureComponent { ); - const inner = sectionIcon ? {component} : component; + const inner = sectionIcon ? {component} : component; return tooltipText ? {inner} : inner; } diff --git a/ui/component/router/view.jsx b/ui/component/router/view.jsx index ea8e5091a..ea72be5a7 100644 --- a/ui/component/router/view.jsx +++ b/ui/component/router/view.jsx @@ -32,9 +32,10 @@ import SignInVerifyPage from 'page/signInVerify'; import ChannelsPage from 'page/channels'; import EmbedWrapperPage from 'page/embedWrapper'; import TopPage from 'page/top'; +import Welcome from 'page/welcome'; +import CreatorDashboard from 'page/creatorDashboard'; import { parseURI } from 'lbry-redux'; import { SITE_TITLE, WELCOME_VERSION } from 'config'; -import Welcome from 'page/welcome'; // Tell the browser we are handling scroll restoration if ('scrollRestoration' in history) { @@ -149,7 +150,7 @@ function AppRouter(props: Props) { - + @@ -174,6 +175,7 @@ function AppRouter(props: Props) { + diff --git a/ui/component/selectChannel/view.jsx b/ui/component/selectChannel/view.jsx index 875e61b75..15aa530b6 100644 --- a/ui/component/selectChannel/view.jsx +++ b/ui/component/selectChannel/view.jsx @@ -13,7 +13,7 @@ type Props = { fetchChannelListMine: () => void, fetchingChannels: boolean, hideAnon: boolean, - includeNew?: boolean, + hideNew: boolean, label?: string, injected?: Array, emailVerified: boolean, @@ -71,7 +71,7 @@ class ChannelSection extends React.PureComponent { render() { const channel = this.state.addingChannel ? 'new' : this.props.channel; - const { fetchingChannels, channels = [], hideAnon, label, injected = [] } = this.props; + const { fetchingChannels, channels = [], hideAnon, hideNew, label, injected = [] } = this.props; const { addingChannel } = this.state; return ( @@ -96,7 +96,7 @@ class ChannelSection extends React.PureComponent { {item} ))} - {!fetchingChannels && } + {!fetchingChannels && !hideNew && } {addingChannel && } diff --git a/ui/constants/pages.js b/ui/constants/pages.js index 5ad175a65..00bbcf6fa 100644 --- a/ui/constants/pages.js +++ b/ui/constants/pages.js @@ -33,3 +33,4 @@ exports.CHANNELS = 'channels'; exports.EMBED = 'embed'; exports.TOP = 'top'; exports.WELCOME = 'welcome'; +exports.CREATOR_DASHBOARD = 'dashboard'; diff --git a/ui/page/creatorDashboard/index.js b/ui/page/creatorDashboard/index.js new file mode 100644 index 000000000..35d76bc8c --- /dev/null +++ b/ui/page/creatorDashboard/index.js @@ -0,0 +1,10 @@ +import { connect } from 'react-redux'; +import { selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux'; +import Welcome from './view'; + +const select = state => ({ + channels: selectMyChannelClaims(state), + fetchingChannels: selectFetchingMyChannels(state), +}); + +export default connect(select)(Welcome); diff --git a/ui/page/creatorDashboard/view.jsx b/ui/page/creatorDashboard/view.jsx new file mode 100644 index 000000000..f0178182f --- /dev/null +++ b/ui/page/creatorDashboard/view.jsx @@ -0,0 +1,125 @@ +// @flow +import * as ICONS from 'constants/icons'; +import React from 'react'; +import Page from 'component/page'; +import { Lbryio } from 'lbryinc'; +import ChannelSelector from 'component/channelSelector'; +import ClaimPreview from 'component/claimPreview'; +import Card from 'component/common/card'; +import Spinner from 'component/spinner'; +import Icon from 'component/common/icon'; +import usePersistedState from 'effects/use-persisted-state'; + +type Props = { + channels: Array, +}; + +export default function CreatorDashboardPage(props: Props) { + const { channels } = props; + const [stats, setStats] = React.useState(); + const [selectedChannelUrl, setSelectedChannelUrl] = usePersistedState('analytics-selected-channel'); + const [fetchingStats, setFetchingStats] = React.useState(false); + const topChannel = + channels && + channels.reduce((top, channel) => { + const topClaimCount = (top && top.meta && top.meta.claims_in_channel) || 0; + const currentClaimCount = (channel && channel.meta && channel.meta.claims_in_channel) || 0; + // $FlowFixMe + return topClaimCount >= currentClaimCount ? top : channel.canonical_url; + }); + + const selectedChannelClaim = channels && channels.find(claim => claim.canonical_url === selectedChannelUrl); + const selectedChannelClaimId = selectedChannelClaim && selectedChannelClaim.claim_id; + + React.useEffect(() => { + // set default channel + if (!selectedChannelUrl && topChannel) { + setSelectedChannelUrl(topChannel); + } + }, [selectedChannelUrl, topChannel]); + + React.useEffect(() => { + if (selectedChannelClaimId && !fetchingStats) { + setFetchingStats(true); + Lbryio.call('reports', 'content', { claim_id: selectedChannelClaimId }) + .then(res => { + setFetchingStats(false); + setStats(res); + }) + .catch(() => { + setFetchingStats(false); + }); + } + }, [selectedChannelClaimId, setFetchingStats]); + + return ( + +
+ { + setStats(null); + setSelectedChannelUrl(newChannelUrl); + }} + /> +
+ {fetchingStats && !stats && ( +
+ +
+ )} + {!fetchingStats && !stats &&
This channel doesn't have any publishes
} + {stats && ( +
+
+ {stats.ChannelSubs} followers} + icon={ICONS.SUBSCRIBE} + subtitle={ +
+ + {stats.ChannelSubChange > 0 ? '+' : '-'} {stats.ChannelSubChange || 0} this week + + {stats.ChannelSubChange > 0 && } +
+ } + /> + {stats.AllContentViews} views} + subtitle={{stats.AllContentViewsChange || 0} this week} + /> +
+ + + Most Viewed Claim +
+ } + body={ + +
+ + + {stats.VideoViewsTopAllTime} views - {stats.VideoViewChangeTopAllTime} new + + {stats.VideoViewChangeTopAllTime > 0 && ( + + )} +
+ } + /> + +
+ } + /> + + )} +
+ ); +} diff --git a/ui/page/fileListPublished/view.jsx b/ui/page/fileListPublished/view.jsx index 51631abfb..79258631f 100644 --- a/ui/page/fileListPublished/view.jsx +++ b/ui/page/fileListPublished/view.jsx @@ -1,4 +1,5 @@ // @flow +import * as PAGES from 'constants/pages'; import React, { useEffect } from 'react'; import Button from 'component/button'; import ClaimList from 'component/claimList'; @@ -28,6 +29,7 @@ function FileListPublished(props: Props) { return ( +