diff --git a/ui/component/channelSelector/view.jsx b/ui/component/channelSelector/view.jsx index 8ae4e07d0..bf19a376d 100644 --- a/ui/component/channelSelector/view.jsx +++ b/ui/component/channelSelector/view.jsx @@ -4,7 +4,7 @@ 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 ChannelTitle from 'component/channelTitle'; import Icon from 'component/common/icon'; type Props = { @@ -14,17 +14,17 @@ type Props = { }; type ListItemProps = { - url: string, + uri: string, isSelected?: boolean, }; function ChannelListItem(props: ListItemProps) { - const { url, isSelected = false } = props; + const { uri, isSelected = false } = props; return (
- - + + {isSelected && }
); @@ -33,7 +33,7 @@ function ChannelListItem(props: ListItemProps) { function ChannelSelector(props: Props) { const { channels, selectedChannelUrl, onChannelSelect } = props; - if (!selectedChannelUrl) { + if (!channels || !selectedChannelUrl) { return null; } @@ -41,13 +41,13 @@ function ChannelSelector(props: Props) {
- + {channels && channels.map(channel => ( onChannelSelect(channel.canonical_url)}> - + ))} diff --git a/ui/component/channelTitle/index.js b/ui/component/channelTitle/index.js new file mode 100644 index 000000000..502068855 --- /dev/null +++ b/ui/component/channelTitle/index.js @@ -0,0 +1,10 @@ +import { connect } from 'react-redux'; +import { makeSelectClaimForUri, makeSelectTitleForUri } from 'lbry-redux'; +import ChannelTitle from './view'; + +const select = (state, props) => ({ + title: makeSelectTitleForUri(props.uri)(state), + claim: makeSelectClaimForUri(props.uri)(state), +}); + +export default connect(select)(ChannelTitle); diff --git a/ui/component/channelTitle/view.jsx b/ui/component/channelTitle/view.jsx new file mode 100644 index 000000000..c7d8fcc1e --- /dev/null +++ b/ui/component/channelTitle/view.jsx @@ -0,0 +1,19 @@ +// @flow +import React from 'react'; + +type Props = { + claim: ?ChannelClaim, + title: ?string, +}; + +function ChannelTitle(props: Props) { + const { title, claim } = props; + + if (!claim) { + return null; + } + + return
{title || claim.name}
; +} + +export default ChannelTitle; diff --git a/ui/component/claimPreviewTitle/index.js b/ui/component/claimPreviewTitle/index.js index e1d05cf4e..18ec751da 100644 --- a/ui/component/claimPreviewTitle/index.js +++ b/ui/component/claimPreviewTitle/index.js @@ -1,10 +1,11 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectTitleForUri } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectTitleForUri, makeSelectIsUriResolving } from 'lbry-redux'; import ClaimPreviewTitle from './view'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state), + isResolvingUri: makeSelectIsUriResolving(props.uri)(state), }); export default connect(select)(ClaimPreviewTitle); diff --git a/ui/component/claimPreviewTitle/view.jsx b/ui/component/claimPreviewTitle/view.jsx index c92fb804b..51b745178 100644 --- a/ui/component/claimPreviewTitle/view.jsx +++ b/ui/component/claimPreviewTitle/view.jsx @@ -10,7 +10,6 @@ type Props = { function ClaimPreviewTitle(props: Props) { const { title, claim } = props; - return (
{claim ? : {__('Nothing here')}} diff --git a/ui/component/creatorAnalytics/index.js b/ui/component/creatorAnalytics/index.js new file mode 100644 index 000000000..2521004a7 --- /dev/null +++ b/ui/component/creatorAnalytics/index.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import { selectMyChannelClaims, selectFetchingMyChannels, doPrepareEdit } from 'lbry-redux'; +import CreatorAnalytics from './view'; + +const select = state => ({ + channels: selectMyChannelClaims(state), + fetchingChannels: selectFetchingMyChannels(state), +}); + +const perform = dispatch => ({ + prepareEdit: channelName => dispatch(doPrepareEdit({ signing_channel: { name: channelName } })), +}); + +export default connect(select, perform)(CreatorAnalytics); diff --git a/ui/component/creatorAnalytics/view.jsx b/ui/component/creatorAnalytics/view.jsx new file mode 100644 index 000000000..f26bdab33 --- /dev/null +++ b/ui/component/creatorAnalytics/view.jsx @@ -0,0 +1,150 @@ +// @flow +import * as ICONS from 'constants/icons'; +import * as PAGES from 'constants/pages'; +import React from 'react'; +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'; +import Button from 'component/button'; +import Yrbl from 'component/yrbl'; +import { useHistory } from 'react-router-dom'; + +type Props = { + channels: Array, + fetchingChannels: boolean, + prepareEdit: string => void, +}; + +export default function CreatorDashboardPage(props: Props) { + const { channels, prepareEdit } = props; + const history = useHistory(); + const [stats, setStats] = React.useState(); + const [selectedChannelUrl, setSelectedChannelUrl] = usePersistedState('analytics-selected-channel'); + const [fetchingStats, setFetchingStats] = React.useState(false); + const hasChannels = channels && channels.length > 0; + const firstChannel = hasChannels && channels[0]; + const firstChannelUrl = firstChannel && (firstChannel.canonical_url || firstChannel.permanent_url); // permanent_url is needed for pending publishes + const selectedChannelClaim = + channels && + channels.find(claim => claim.canonical_url === selectedChannelUrl || claim.permanent_url === selectedChannelUrl); + const selectedChannelClaimId = selectedChannelClaim && selectedChannelClaim.claim_id; + const channelFoundForSelectedChannelUrl = + channels && + channels.find(channel => { + return selectedChannelUrl === channel.canonical_url || selectedChannelUrl === channel.permanent_url; + }); + + React.useEffect(() => { + // set default channel + if ((!selectedChannelUrl || !channelFoundForSelectedChannelUrl) && firstChannelUrl) { + setSelectedChannelUrl(firstChannelUrl); + } + }, [selectedChannelUrl, firstChannelUrl, channelFoundForSelectedChannelUrl]); + + React.useEffect(() => { + if (selectedChannelClaimId) { + setFetchingStats(true); + Lbryio.call('reports', 'content', { claim_id: selectedChannelClaimId }) + .then(res => { + setFetchingStats(false); + setStats(res); + }) + .catch(() => { + setFetchingStats(false); + }); + } + }, [selectedChannelClaimId, setFetchingStats, setStats]); + + return ( + +
+ { + setStats(null); + setSelectedChannelUrl(newChannelUrl); + }} + /> +
+ {fetchingStats && !stats && ( +
+ +
+ )} + {!fetchingStats && !stats && ( +
+ { + if (selectedChannelClaim) { + prepareEdit(selectedChannelClaim.name); + history.push(`/$/${PAGES.PUBLISH}`); + } + }} + /> + } + /> +
+ )} + {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} this week + + {stats.VideoViewChangeTopAllTime > 0 && ( + + )} +
+ } + /> +
+ + } + /> +
+ )} + + ); +} diff --git a/ui/page/creatorDashboard/index.js b/ui/page/creatorDashboard/index.js index 35d76bc8c..42371ff32 100644 --- a/ui/page/creatorDashboard/index.js +++ b/ui/page/creatorDashboard/index.js @@ -1,10 +1,16 @@ +import * as MODALS from 'constants/modal_types'; import { connect } from 'react-redux'; import { selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux'; -import Welcome from './view'; +import { doOpenModal } from 'redux/actions/app'; +import CreatorDashboardPage from './view'; const select = state => ({ channels: selectMyChannelClaims(state), fetchingChannels: selectFetchingMyChannels(state), }); -export default connect(select)(Welcome); +const perform = dispatch => ({ + openChannelCreateModal: () => dispatch(doOpenModal(MODALS.CREATE_CHANNEL)), +}); + +export default connect(select, perform)(CreatorDashboardPage); diff --git a/ui/page/creatorDashboard/view.jsx b/ui/page/creatorDashboard/view.jsx index f0178182f..07cff9633 100644 --- a/ui/page/creatorDashboard/view.jsx +++ b/ui/page/creatorDashboard/view.jsx @@ -1,125 +1,39 @@ // @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'; +import Button from 'component/button'; +import CreatorAnalytics from 'component/creatorAnalytics'; type Props = { channels: Array, + fetchingChannels: boolean, + openChannelCreateModal: () => void, }; 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]); + const { channels, fetchingChannels, openChannelCreateModal } = props; return ( -
- { - setStats(null); - setSelectedChannelUrl(newChannelUrl); - }} - /> -
- {fetchingStats && !stats && ( + {fetchingChannels && (
)} - {!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 && ( - - )} -
- } - /> - -
- } - /> - + {!fetchingChannels && (!channels || !channels.length) && ( +
+
+

{__("You haven't created a channel yet, let's fix that!")}

+
+
+
+
)} + + {!fetchingChannels && channels && channels.length && }
); } diff --git a/ui/scss/component/_yrbl.scss b/ui/scss/component/_yrbl.scss index 1689936e5..c637a66e9 100644 --- a/ui/scss/component/_yrbl.scss +++ b/ui/scss/component/_yrbl.scss @@ -9,7 +9,7 @@ .yrbl { height: 20rem; - margin-right: var(--spacing-large); + margin-right: calc(var(--spacing-xlarge) * 2); @media (max-width: $breakpoint-small) { height: 10rem; diff --git a/ui/scss/component/section.scss b/ui/scss/component/section.scss index 68ca05e56..b879cbf9b 100644 --- a/ui/scss/component/section.scss +++ b/ui/scss/component/section.scss @@ -12,7 +12,7 @@ } .section--small { - max-width: 35rem; + max-width: 40rem; } .section__header {