From 6bcec9b142b28c2e36ef2f794feeb2cd8f9095cb Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 26 Sep 2017 15:41:48 -0700 Subject: [PATCH 01/23] Setup view for community analytics --- .../communityAnalytics/components/header.js | 29 ++++++ .../components/topMembers.js | 0 src/views/communityAnalytics/index.js | 98 +++++++++++++++++++ src/views/communityAnalytics/queries.js | 23 +++++ src/views/communityAnalytics/style.js | 26 +++++ 5 files changed, 176 insertions(+) create mode 100644 src/views/communityAnalytics/components/header.js create mode 100644 src/views/communityAnalytics/components/topMembers.js create mode 100644 src/views/communityAnalytics/index.js create mode 100644 src/views/communityAnalytics/queries.js create mode 100644 src/views/communityAnalytics/style.js diff --git a/src/views/communityAnalytics/components/header.js b/src/views/communityAnalytics/components/header.js new file mode 100644 index 0000000000..a8a9e843c7 --- /dev/null +++ b/src/views/communityAnalytics/components/header.js @@ -0,0 +1,29 @@ +// @flow +import * as React from 'react'; +import { StyledHeader, Heading, Subheading } from '../style'; +import { Avatar } from '../../../components/avatar'; +import { FlexCol } from '../../../components/globals'; + +type Props = { + community: { + name: string, + profilePhoto: string, + }, +}; + +class Header extends React.Component { + render() { + const { community: { name, profilePhoto } } = this.props; + return ( + + + + {name} + Analytics + + + ); + } +} + +export default Header; diff --git a/src/views/communityAnalytics/components/topMembers.js b/src/views/communityAnalytics/components/topMembers.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/views/communityAnalytics/index.js b/src/views/communityAnalytics/index.js new file mode 100644 index 0000000000..1f4b5a42f6 --- /dev/null +++ b/src/views/communityAnalytics/index.js @@ -0,0 +1,98 @@ +// @flow +import React from 'react'; +//$flowignore +import compose from 'recompose/compose'; +//$flowignore +import pure from 'recompose/pure'; +// $flowignore +import { connect } from 'react-redux'; +// $flowignore +import { Link } from 'react-router-dom'; +import { getThisCommunity } from './queries'; +import { Loading } from '../../components/loading'; +import AppViewWrapper from '../../components/appViewWrapper'; +import ViewError from '../../components/viewError'; +import viewNetworkHandler from '../../components/viewNetworkHandler'; +import { Button, OutlineButton, ButtonRow } from '../../components/buttons'; +import Titlebar from '../titlebar'; +import Header from './components/header'; + +type Props = { + match: { + params: { + communitySlug: string, + }, + }, + data: { + community: { + name: string, + profilePhoto: string, + }, + }, + isLoading: boolean, + hasError: boolean, +}; + +type State = { + timeframe: 'weekly' | 'monthly', +}; + +class CommunitySettings extends React.Component { + render() { + const { + match: { params: { communitySlug } }, + data: { community }, + isLoading, + hasError, + } = this.props; + + if (community) { + return ( + + + +
+ + ); + } + + if (isLoading) { + return ; + } + + return ( + + + + + + + Take me back + + + + + + + + + ); + } +} + +export default compose(connect(), getThisCommunity, viewNetworkHandler, pure)( + CommunitySettings +); diff --git a/src/views/communityAnalytics/queries.js b/src/views/communityAnalytics/queries.js new file mode 100644 index 0000000000..89eab46444 --- /dev/null +++ b/src/views/communityAnalytics/queries.js @@ -0,0 +1,23 @@ +// @flow +// $flowignore +import { graphql, gql } from 'react-apollo'; + +export const getThisCommunity = graphql( + gql` + query community($slug: String) { + community(slug: $slug) { + id + name + profilePhoto + } + } + `, + { + options: props => ({ + variables: { + slug: props.match.params.communitySlug.toLowerCase(), + }, + fetchPolicy: 'network-only', + }), + } +); diff --git a/src/views/communityAnalytics/style.js b/src/views/communityAnalytics/style.js new file mode 100644 index 0000000000..76e3ab24bf --- /dev/null +++ b/src/views/communityAnalytics/style.js @@ -0,0 +1,26 @@ +// @flow +// $flowignore +import styled from 'styled-components'; + +export const Heading = styled.h1` + margin-left: 16px; + font-size: 24px; + color: ${props => props.theme.text.default}; + font-weight: 800; +`; + +export const Subheading = styled.h3` + margin-left: 16px; + font-size: 14px; + color: ${props => props.theme.text.alt}; + font-weight: 600; +`; + +export const StyledHeader = styled.div` + display: flex; + padding: 32px; + border-bottom: 1px solid ${props => props.theme.bg.border}; + background: ${props => props.theme.bg.default}; + width: 100%; + align-items: center; +`; From 8c06018d39dfa6228646355fdcae5b4dcbe4c414 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 26 Sep 2017 15:44:27 -0700 Subject: [PATCH 02/23] Fix route, lost in a git reset --- src/components/buttons/index.js | 5 +++++ src/components/buttons/style.js | 9 +++++++++ src/routes.js | 16 ++++++++++++---- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/components/buttons/index.js b/src/components/buttons/index.js index 330ac799d0..f035a7c528 100644 --- a/src/components/buttons/index.js +++ b/src/components/buttons/index.js @@ -8,6 +8,7 @@ import { StyledOutlineButton, StyledFauxOutlineButton, SpinnerContainer, + StyledButtonRow, } from './style'; import { Spinner } from '../globals'; import Icon from '../icons'; @@ -119,3 +120,7 @@ export const IconButton = (props: IconProps) => ( /> ); + +export const ButtonRow = props => ( + {props.children} +); diff --git a/src/components/buttons/style.js b/src/components/buttons/style.js index ecb8dca0ff..4be0e306fa 100644 --- a/src/components/buttons/style.js +++ b/src/components/buttons/style.js @@ -201,3 +201,12 @@ export const SpinnerContainer = styled.div` height: 32px; position: relative; `; + +export const StyledButtonRow = styled.div` + display: flex; + justify-content: center; + align-items: center; + button { + margin: 0 8px; + } +`; diff --git a/src/routes.js b/src/routes.js index e857f491a3..8e6b2fea60 100644 --- a/src/routes.js +++ b/src/routes.js @@ -25,8 +25,9 @@ import StyleGuide from './views/pages/styleGuide'; import Dashboard from './views/dashboard'; import Notifications from './views/notifications'; import UserSettings from './views/userSettings'; -import communitySettings from './views/communitySettings'; -import channelSettings from './views/channelSettings'; +import CommunitySettings from './views/communitySettings'; +import CommunityAnalytics from './views/communityAnalytics'; +import ChannelSettings from './views/channelSettings'; import NewCommunity from './views/newCommunity'; import Splash from './views/splash'; import signedOutFallback from './helpers/signed-out-fallback'; @@ -63,10 +64,13 @@ const MessagesFallback = signedOutFallback(DirectMessages, () => ( const UserSettingsFallback = signedOutFallback(UserSettings, () => ( )); -const CommunitySettingsFallback = signedOutFallback(communitySettings, () => ( +const CommunitySettingsFallback = signedOutFallback(CommunitySettings, () => ( )); -const ChannelSettingsFallback = signedOutFallback(channelSettings, () => ( +const CommunityAnalyticsFallback = signedOutFallback(CommunityAnalytics, () => ( + +)); +const ChannelSettingsFallback = signedOutFallback(ChannelSettings, () => ( )); const NotificationsFallback = signedOutFallback(Notifications, () => ( @@ -153,6 +157,10 @@ class Routes extends React.Component<{}> { path="/:communitySlug/:channelSlug/settings" component={ChannelSettingsFallback} /> + Date: Wed, 27 Sep 2017 09:51:20 -0700 Subject: [PATCH 03/23] Members, conversations, and top members modules in community analytics --- ...-communityid-index-on-reputation-events.js | 15 ++ iris/models/community.js | 54 ++++++++ iris/models/reputationEvents.js | 33 +++++ iris/models/utils.js | 2 +- iris/queries/community.js | 128 ++++++++++++++++++ iris/queries/user.js | 21 ++- iris/types/Community.js | 3 + iris/types/Meta.js | 13 -- iris/types/general.js | 13 ++ src/components/listItems/index.js | 18 ++- .../components/conversationGrowth.js | 71 ++++++++++ .../communityAnalytics/components/header.js | 7 +- .../components/memberGrowth.js | 69 ++++++++++ .../communityAnalytics/components/queries.js | 62 +++++++++ .../components/topMembers.js | 58 ++++++++ src/views/communityAnalytics/index.js | 29 +++- src/views/communityAnalytics/queries.js | 123 ++++++++++++++++- src/views/communityAnalytics/style.js | 79 ++++++++++- src/views/communityAnalytics/utils.js | 43 ++++++ 19 files changed, 803 insertions(+), 38 deletions(-) create mode 100644 iris/migrations/20170927002438-communityid-index-on-reputation-events.js create mode 100644 iris/models/reputationEvents.js create mode 100644 src/views/communityAnalytics/components/conversationGrowth.js create mode 100644 src/views/communityAnalytics/components/memberGrowth.js create mode 100644 src/views/communityAnalytics/components/queries.js create mode 100644 src/views/communityAnalytics/utils.js diff --git a/iris/migrations/20170927002438-communityid-index-on-reputation-events.js b/iris/migrations/20170927002438-communityid-index-on-reputation-events.js new file mode 100644 index 0000000000..330ea85043 --- /dev/null +++ b/iris/migrations/20170927002438-communityid-index-on-reputation-events.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(r, conn) { + return Promise.all([ + r + .db('spectrum') + .table('reputationEvents') + .indexCreate('communityId') + .run(conn), + ]); +}; + +exports.down = function(r, conn) { + return Promise.resolve(); +}; diff --git a/iris/models/community.js b/iris/models/community.js index 255ad1f582..60868ecbac 100644 --- a/iris/models/community.js +++ b/iris/models/community.js @@ -3,6 +3,7 @@ const { db } = require('./db'); // $FlowFixMe import UserError from '../utils/UserError'; import { createChannel, deleteChannel } from './channel'; +import { parseRange } from './utils'; import { uploadImage } from '../utils/s3'; import getRandomDefaultPhoto from '../utils/get-random-default-photo'; import { addQueue } from '../utils/workerQueue'; @@ -78,6 +79,15 @@ const getCommunityMetaData = (communityId: string): Promise> => { return Promise.all([getChannelCount, getMemberCount]); }; +const getMemberCount = (communityId: string): Promise => { + return db + .table('usersCommunities') + .getAll(communityId, { index: 'communityId' }) + .filter({ isBlocked: false, isMember: true }) + .count() + .run(); +}; + export type CreateCommunityArguments = { input: { name: string, @@ -575,6 +585,47 @@ const searchThreadsInCommunity = ( .run(); }; +const getThreadCount = async (communityId: string) => { + return db + .table('threads') + .getAll(communityId, { index: 'communityId' }) + .filter(thread => db.not(thread.hasFields('deletedAt'))) + .count() + .run(); +}; + +export const getCommunityGrowth = async ( + table: string, + range: string, + field: string, + communityId: string, + filter?: mixed +) => { + const { current, previous } = parseRange(range); + const currentPeriodCount = await db + .table(table) + .getAll(communityId, { index: 'communityId' }) + .filter(db.row(field).during(db.now().sub(current), db.now())) + .filter(filter ? filter : '') + .count() + .run(); + + const prevPeriodCount = await db + .table(table) + .getAll(communityId, { index: 'communityId' }) + .filter(db.row(field).during(db.now().sub(previous), db.now().sub(current))) + .filter(filter ? filter : '') + .count() + .run(); + + const rate = (await (currentPeriodCount - prevPeriodCount)) / prevPeriodCount; + return { + currentPeriodCount, + prevPeriodCount, + growth: Math.round(rate * 100), + }; +}; + module.exports = { getCommunities, getCommunitiesBySlug, @@ -591,4 +642,7 @@ module.exports = { getRecentCommunities, getCommunitiesBySearchString, searchThreadsInCommunity, + getMemberCount, + getThreadCount, + getCommunityGrowth, }; diff --git a/iris/models/reputationEvents.js b/iris/models/reputationEvents.js new file mode 100644 index 0000000000..1df6a9cb36 --- /dev/null +++ b/iris/models/reputationEvents.js @@ -0,0 +1,33 @@ +// @flow +import { db } from './db'; +import { parseRange } from './utils'; + +export const getTopMembersInCommunity = ( + communityId: string +): Promise> => { + const { current } = parseRange('weekly'); + + return db + .table('reputationEvents') + .getAll(communityId, { index: 'communityId' }) + .filter(db.row('timestamp').during(db.now().sub(current), db.now())) + .group('userId') + .run() + .then(results => { + if (!results) return null; + const sorted = results + .map(c => ({ + userId: c.group, + reputation: c.reduction.reduce((a, b) => a.score + b.score), + })) + .sort((a, b) => { + const bc = parseInt(b.reputation, 10); + const ac = parseInt(a.reputation, 10); + return bc <= ac ? -1 : 1; + }) + .slice(0, 10) + .map(c => c.userId); + + return sorted; + }); +}; diff --git a/iris/models/utils.js b/iris/models/utils.js index bb93cf7722..85832a256b 100644 --- a/iris/models/utils.js +++ b/iris/models/utils.js @@ -26,7 +26,7 @@ export const listenToNewDocumentsIn = (table, cb) => { ); }; -const parseRange = timeframe => { +export const parseRange = timeframe => { switch (timeframe) { case 'weekly': { return { current: 60 * 60 * 24 * 7, previous: 60 * 60 * 24 * 14 }; diff --git a/iris/queries/community.js b/iris/queries/community.js index 4b796f6cae..313386f1d4 100644 --- a/iris/queries/community.js +++ b/iris/queries/community.js @@ -9,7 +9,11 @@ const { getRecentCommunities, getCommunitiesBySearchString, searchThreadsInCommunity, + getMemberCount, + getThreadCount, + getCommunityGrowth, } = require('../models/community'); +const { getTopMembersInCommunity } = require('../models/reputationEvents'); const { getUserPermissionsInCommunity, getMembersInCommunity, @@ -257,5 +261,129 @@ module.exports = { return !filtered || filtered.length === 0 ? false : true; }); }, + memberGrowth: async ( + { id }: { id: string }, + __: any, + { user }: GraphQLContext + ) => { + const currentUser = user; + + if (!currentUser) { + return new UserError('You must be signed in to continue.'); + } + + const { isOwner } = await getUserPermissionsInCommunity( + id, + currentUser.id + ); + + if (!isOwner) { + return new UserError( + 'You must be the owner of this community to view analytics.' + ); + } + + return { + count: await getMemberCount(id), + weeklyGrowth: await getCommunityGrowth( + 'usersCommunities', + 'weekly', + 'createdAt', + id, + { + isMember: true, + } + ), + monthlyGrowth: await getCommunityGrowth( + 'usersCommunities', + 'monthly', + 'createdAt', + id, + { + isMember: true, + } + ), + quarterlyGrowth: await getCommunityGrowth( + 'usersCommunities', + 'quarterly', + 'createdAt', + id, + { + isMember: true, + } + ), + }; + }, + conversationGrowth: async ( + { id }: { id: string }, + __: any, + { user }: GraphQLContext + ) => { + const currentUser = user; + + if (!currentUser) { + return new UserError('You must be signed in to continue.'); + } + + const { isOwner } = await getUserPermissionsInCommunity( + id, + currentUser.id + ); + + if (!isOwner) { + return new UserError( + 'You must be the owner of this community to view analytics.' + ); + } + + return { + count: await getThreadCount(id), + weeklyGrowth: await getCommunityGrowth( + 'threads', + 'weekly', + 'createdAt', + id + ), + monthlyGrowth: await getCommunityGrowth( + 'threads', + 'monthly', + 'createdAt', + id + ), + quarterlyGrowth: await getCommunityGrowth( + 'threads', + 'quarterly', + 'createdAt', + id + ), + }; + }, + topMembers: async ( + { id }: { id: string }, + __: any, + { user, loaders }: GraphQLContext + ) => { + const currentUser = user; + + if (!currentUser) { + return new UserError('You must be signed in to continue.'); + } + + const { isOwner } = await getUserPermissionsInCommunity( + id, + currentUser.id + ); + + if (!isOwner) { + return new UserError( + 'You must be the owner of this community to view analytics.' + ); + } + + return getTopMembersInCommunity(id).then(users => { + if (!users) return []; + return loaders.user.loadMany(users); + }); + }, }, }; diff --git a/iris/queries/user.js b/iris/queries/user.js index 43d4afe510..93f7b1a72a 100644 --- a/iris/queries/user.js +++ b/iris/queries/user.js @@ -7,7 +7,10 @@ const { getUsersBySearchString, } = require('../models/user'); const { getUsersSettings } = require('../models/usersSettings'); -const { getCommunitiesByUser } = require('../models/community'); +const { + getCommunitiesByUser, + getCommunitiesBySlug, +} = require('../models/community'); const { getChannelsByUser } = require('../models/channel'); const { getThread, @@ -249,6 +252,22 @@ module.exports = { isOwner, }; } + case 'getCommunityTopMembers': { + const communities = await getCommunitiesBySlug([ + info.variableValues.slug, + ]); + const { id } = communities[0]; + const { + reputation, + isModerator, + isOwner, + } = await getUserPermissionsInCommunity(id, user.id); + return { + reputation, + isModerator, + isOwner, + }; + } } }; diff --git a/iris/types/Community.js b/iris/types/Community.js index 9119850b94..0101694309 100644 --- a/iris/types/Community.js +++ b/iris/types/Community.js @@ -104,6 +104,9 @@ const Community = /* GraphQL */ ` invoices: [Invoice] recurringPayments: [RecurringPayment] isPro: Boolean + memberGrowth: GrowthData + conversationGrowth: GrowthData + topMembers: [User] } extend type Query { diff --git a/iris/types/Meta.js b/iris/types/Meta.js index d68a29a479..db10d50f3a 100644 --- a/iris/types/Meta.js +++ b/iris/types/Meta.js @@ -1,17 +1,4 @@ const Meta = /* GraphQL */ ` - type GrowthDataCounts { - growth: Float - currentPeriodCount: Int - prevPeriodCount: Int - } - - type GrowthData { - count: Int - weeklyGrowth: GrowthDataCounts - monthlyGrowth: GrowthDataCounts - quarterlyGrowth: GrowthDataCounts - } - type Meta { usersGrowth: GrowthData communitiesGrowth: GrowthData diff --git a/iris/types/general.js b/iris/types/general.js index d95fb267e0..60eb138038 100644 --- a/iris/types/general.js +++ b/iris/types/general.js @@ -45,6 +45,19 @@ const general = /* GraphQL */ ` isModerator: Boolean isOwner: Boolean } + + type GrowthDataCounts { + growth: Float + currentPeriodCount: Int + prevPeriodCount: Int + } + + type GrowthData { + count: Int + weeklyGrowth: GrowthDataCounts + monthlyGrowth: GrowthDataCounts + quarterlyGrowth: GrowthDataCounts + } `; module.exports = general; diff --git a/src/components/listItems/index.js b/src/components/listItems/index.js index 3b22529b84..9bed2e123b 100644 --- a/src/components/listItems/index.js +++ b/src/components/listItems/index.js @@ -173,12 +173,14 @@ export const UserListItem = ({ @{user.username} 路{' '} )} - {user.totalReputation && ( + {(user.totalReputation || user.contextPermissions) && ( - {user.contextPermissions - ? user.contextPermissions.reputation.toLocaleString() - : user.totalReputation.toLocaleString()} + {user.contextPermissions ? ( + user.contextPermissions.reputation.toLocaleString() + ) : ( + user.totalReputation.toLocaleString() + )} )} @@ -231,9 +233,11 @@ class InvoiceListItemPure extends Component { .replace(/(\d)(?=(\d{3})+\.)/g, '$1,')} - {invoice.paidAt - ? `Paid on ${convertTimestampToDate(invoice.paidAt * 1000)}` - : 'Unpaid'}{' '} + {invoice.paidAt ? ( + `Paid on ${convertTimestampToDate(invoice.paidAt * 1000)}` + ) : ( + 'Unpaid' + )}{' '} 路 {invoice.sourceBrand} {invoice.sourceLast4} diff --git a/src/views/communityAnalytics/components/conversationGrowth.js b/src/views/communityAnalytics/components/conversationGrowth.js new file mode 100644 index 0000000000..4fa7fd7119 --- /dev/null +++ b/src/views/communityAnalytics/components/conversationGrowth.js @@ -0,0 +1,71 @@ +// @flow +import * as React from 'react'; +// $FlowFixMe +import pure from 'recompose/pure'; +// $FlowFixMe +import compose from 'recompose/compose'; +import viewNetworkHandler from '../../../components/viewNetworkHandler'; +import { Loading } from '../../../components/loading'; +import { SectionCard, SectionSubtitle, SectionTitle } from '../style'; +import { getCommunityConversationGrowth } from '../queries'; +import { parseGrowth } from '../utils'; + +type GrowthType = { + growth: number, + currentPeriodCount: number, + prevPeriodCount: number, +}; + +type Props = { + isLoading: boolean, + data: { + community: { + conversationGrowth: { + count: number, + weeklyGrowth: GrowthType, + monthlyGrowth: GrowthType, + quarterlyGrowth: GrowthType, + }, + }, + }, +}; + +class ConversationGrowth extends React.Component { + render() { + const { data, data: { community }, isLoading } = this.props; + + if (community) { + const { + count, + weeklyGrowth, + monthlyGrowth, + quarterlyGrowth, + } = community.conversationGrowth; + return ( + + Conversations + {count} + {parseGrowth(weeklyGrowth, 'weekly')} + {parseGrowth(monthlyGrowth, 'monthly')} + {parseGrowth(quarterlyGrowth, 'quarterly')} + + ); + } + + if (isLoading) { + return ( + + + + ); + } + + return null; + } +} + +export default compose( + getCommunityConversationGrowth, + viewNetworkHandler, + pure +)(ConversationGrowth); diff --git a/src/views/communityAnalytics/components/header.js b/src/views/communityAnalytics/components/header.js index a8a9e843c7..76e0786098 100644 --- a/src/views/communityAnalytics/components/header.js +++ b/src/views/communityAnalytics/components/header.js @@ -1,8 +1,7 @@ // @flow import * as React from 'react'; -import { StyledHeader, Heading, Subheading } from '../style'; +import { StyledHeader, Heading, Subheading, HeaderText } from '../style'; import { Avatar } from '../../../components/avatar'; -import { FlexCol } from '../../../components/globals'; type Props = { community: { @@ -17,10 +16,10 @@ class Header extends React.Component { return ( - + {name} Analytics - + ); } diff --git a/src/views/communityAnalytics/components/memberGrowth.js b/src/views/communityAnalytics/components/memberGrowth.js new file mode 100644 index 0000000000..24023033ba --- /dev/null +++ b/src/views/communityAnalytics/components/memberGrowth.js @@ -0,0 +1,69 @@ +// @flow +import * as React from 'react'; +// $FlowFixMe +import pure from 'recompose/pure'; +// $FlowFixMe +import compose from 'recompose/compose'; +import viewNetworkHandler from '../../../components/viewNetworkHandler'; +import { Loading } from '../../../components/loading'; +import { SectionCard, SectionSubtitle, SectionTitle } from '../style'; +import { getCommunityMemberGrowth } from '../queries'; +import { parseGrowth } from '../utils'; + +type GrowthType = { + growth: number, + currentPeriodCount: number, + prevPeriodCount: number, +}; + +type Props = { + isLoading: boolean, + data: { + community: { + memberGrowth: { + count: number, + weeklyGrowth: GrowthType, + monthlyGrowth: GrowthType, + quarterlyGrowth: GrowthType, + }, + }, + }, +}; + +class MemberGrowth extends React.Component { + render() { + const { data, data: { community }, isLoading } = this.props; + + if (community) { + const { + count, + weeklyGrowth, + monthlyGrowth, + quarterlyGrowth, + } = community.memberGrowth; + return ( + + Members + {count} + {parseGrowth(weeklyGrowth, 'weekly')} + {parseGrowth(monthlyGrowth, 'monthly')} + {parseGrowth(quarterlyGrowth, 'quarterly')} + + ); + } + + if (isLoading) { + return ( + + + + ); + } + + return null; + } +} + +export default compose(getCommunityMemberGrowth, viewNetworkHandler, pure)( + MemberGrowth +); diff --git a/src/views/communityAnalytics/components/queries.js b/src/views/communityAnalytics/components/queries.js new file mode 100644 index 0000000000..32253a9c4e --- /dev/null +++ b/src/views/communityAnalytics/components/queries.js @@ -0,0 +1,62 @@ +import { graphql, gql } from 'react-apollo'; +import { communityInfoFragment } from '../../api/fragments/community/communityInfo'; +import { channelInfoFragment } from '../../api/fragments/channel/channelInfo'; +import { channelMetaDataFragment } from '../../api/fragments/channel/channelMetaData'; + +export const getThisCommunity = graphql( + gql` + query thisCommunity($slug: String) { + community(slug: $slug) { + ...communityInfo + recurringPayments { + plan + amount + createdAt + status + } + } + } + ${communityInfoFragment} + `, + { + options: props => ({ + variables: { + slug: props.match.params.communitySlug.toLowerCase(), + }, + fetchPolicy: 'network-only', + }), + } +); + +export const GET_COMMUNITY_CHANNELS_QUERY = gql` + query getCommunityChannels($slug: String) { + community(slug: $slug) { + ...communityInfo + channelConnection { + edges { + node { + ...channelInfo + ...channelMetaData + } + } + } + } + } + ${channelInfoFragment} + ${communityInfoFragment} + ${channelMetaDataFragment} +`; + +export const GET_COMMUNITY_CHANNELS_OPTIONS = { + options: ({ communitySlug }: { communitySlug: string }) => ({ + variables: { + slug: communitySlug.toLowerCase(), + }, + fetchPolicy: 'cache-and-network', + }), +}; + +export const getCommunityChannels = graphql( + GET_COMMUNITY_CHANNELS_QUERY, + GET_COMMUNITY_CHANNELS_OPTIONS +); diff --git a/src/views/communityAnalytics/components/topMembers.js b/src/views/communityAnalytics/components/topMembers.js index e69de29bb2..c71884a477 100644 --- a/src/views/communityAnalytics/components/topMembers.js +++ b/src/views/communityAnalytics/components/topMembers.js @@ -0,0 +1,58 @@ +// @flow +import * as React from 'react'; +// $FlowFixMe +import pure from 'recompose/pure'; +// $FlowFixMe +import compose from 'recompose/compose'; +import viewNetworkHandler from '../../../components/viewNetworkHandler'; +import { Loading } from '../../../components/loading'; +import { UserListItem } from '../../../components/listItems'; +import { SectionCard, SectionSubtitle, SectionTitle } from '../style'; +import { getCommunityTopMembers } from '../queries'; + +type User = { + id: string, + profilePhoto: string, + name: string, + username: string, +}; + +type Props = { + isLoading: boolean, + data: { + community: { + topMembers: Array, + }, + }, +}; + +class ConversationGrowth extends React.Component { + render() { + const { data: { community }, isLoading } = this.props; + + if (community && community.topMembers.length > 0) { + return ( + + Top members this week + {community.topMembers.map(user => { + return ; + })} + + ); + } + + if (isLoading) { + return ( + + + + ); + } + + return null; + } +} + +export default compose(getCommunityTopMembers, viewNetworkHandler, pure)( + ConversationGrowth +); diff --git a/src/views/communityAnalytics/index.js b/src/views/communityAnalytics/index.js index 1f4b5a42f6..90c30c2c94 100644 --- a/src/views/communityAnalytics/index.js +++ b/src/views/communityAnalytics/index.js @@ -1,12 +1,12 @@ // @flow import React from 'react'; -//$flowignore +// $FlowFixMe import compose from 'recompose/compose'; -//$flowignore +//$FlowFixMe import pure from 'recompose/pure'; -// $flowignore +// $FlowFixMe import { connect } from 'react-redux'; -// $flowignore +// $FlowFixMe import { Link } from 'react-router-dom'; import { getThisCommunity } from './queries'; import { Loading } from '../../components/loading'; @@ -16,6 +16,10 @@ import viewNetworkHandler from '../../components/viewNetworkHandler'; import { Button, OutlineButton, ButtonRow } from '../../components/buttons'; import Titlebar from '../titlebar'; import Header from './components/header'; +import MemberGrowth from './components/memberGrowth'; +import ConversationGrowth from './components/conversationGrowth'; +import TopMembers from './components/topMembers'; +import { View, SectionsContainer, Column } from './style'; type Props = { match: { @@ -27,6 +31,7 @@ type Props = { community: { name: string, profilePhoto: string, + slug: string, }, }, isLoading: boolean, @@ -56,7 +61,21 @@ class CommunitySettings extends React.Component { noComposer /> -
+ +
+ + + + + + + + + + + + + ); } diff --git a/src/views/communityAnalytics/queries.js b/src/views/communityAnalytics/queries.js index 89eab46444..08eeaac8df 100644 --- a/src/views/communityAnalytics/queries.js +++ b/src/views/communityAnalytics/queries.js @@ -1,16 +1,17 @@ // @flow -// $flowignore +// $FlowFixMe import { graphql, gql } from 'react-apollo'; +import { communityInfoFragment } from '../../api/fragments/community/communityInfo'; +import { userInfoFragment } from '../../api/fragments/user/userInfo'; export const getThisCommunity = graphql( gql` query community($slug: String) { community(slug: $slug) { - id - name - profilePhoto + ...communityInfo } } + ${communityInfoFragment} `, { options: props => ({ @@ -21,3 +22,117 @@ export const getThisCommunity = graphql( }), } ); + +const COMMUNITY_GROWTH_QUERY = gql` + query getCommunityMemberGrowth($slug: String) { + community(slug: $slug) { + ...communityInfo + memberGrowth { + count + weeklyGrowth { + growth + currentPeriodCount + prevPeriodCount + } + monthlyGrowth { + growth + currentPeriodCount + prevPeriodCount + } + quarterlyGrowth { + growth + currentPeriodCount + prevPeriodCount + } + } + } + } + ${communityInfoFragment} +`; + +const COMMUNITY_GROWTH_OPTIONS = { + options: ({ communitySlug }: { communitySlug: string }) => ({ + variables: { + slug: communitySlug.toLowerCase(), + }, + fetchPolicy: 'cache-and-network', + }), +}; + +export const getCommunityMemberGrowth = graphql( + COMMUNITY_GROWTH_QUERY, + COMMUNITY_GROWTH_OPTIONS +); + +const COMMUNITY_CONVERSATION_GROWTH_QUERY = gql` + query getCommunityConversationGrowth($slug: String) { + community(slug: $slug) { + ...communityInfo + conversationGrowth { + count + weeklyGrowth { + growth + currentPeriodCount + prevPeriodCount + } + monthlyGrowth { + growth + currentPeriodCount + prevPeriodCount + } + quarterlyGrowth { + growth + currentPeriodCount + prevPeriodCount + } + } + } + } + ${communityInfoFragment} +`; + +const COMMUNITY_CONVERSATION_GROWTH_OPTIONS = { + options: ({ communitySlug }: { communitySlug: string }) => ({ + variables: { + slug: communitySlug.toLowerCase(), + }, + fetchPolicy: 'cache-and-network', + }), +}; + +export const getCommunityConversationGrowth = graphql( + COMMUNITY_CONVERSATION_GROWTH_QUERY, + COMMUNITY_CONVERSATION_GROWTH_OPTIONS +); + +const COMMUNITY_TOP_MEMBERS_QUERY = gql` + query getCommunityTopMembers($slug: String) { + community(slug: $slug) { + ...communityInfo + topMembers { + ...userInfo + contextPermissions { + reputation + isOwner + isModerator + } + } + } + } + ${communityInfoFragment} + ${userInfoFragment} +`; + +const COMMUNITY_TOP_MEMBERS_OPTIONS = { + options: ({ communitySlug }: { communitySlug: string }) => ({ + variables: { + slug: communitySlug.toLowerCase(), + }, + fetchPolicy: 'cache-and-network', + }), +}; + +export const getCommunityTopMembers = graphql( + COMMUNITY_TOP_MEMBERS_QUERY, + COMMUNITY_TOP_MEMBERS_OPTIONS +); diff --git a/src/views/communityAnalytics/style.js b/src/views/communityAnalytics/style.js index 76e3ab24bf..8f4425a22e 100644 --- a/src/views/communityAnalytics/style.js +++ b/src/views/communityAnalytics/style.js @@ -1,19 +1,86 @@ // @flow -// $flowignore +// $FlowFixMe import styled from 'styled-components'; +export const View = styled.div` + display: flex; + flex-direction: column; + flex: 1; + + @media (max-width: 768px) { + width: 100%; + } +`; + +export const SectionsContainer = styled.div` + display: flex; + flex: 1 0 auto; + flex-wrap: wrap; +`; + +export const Column = styled.div` + display: flex; + flex-direction: column; + padding: 8px; + flex: 1 0 33%; + + @media (max-width: 768px) { + flex: 1 0 100%; + padding-top: 0; + padding-bottom: 0; + + &:first-of-type { + padding-top: 8px; + } + } +`; + +export const SectionCard = styled.div` + border-radius: 4px; + border: 1px solid ${props => props.theme.bg.border}; + background: ${props => props.theme.bg.default}; + margin-bottom: 8px; + padding: 16px; + display: flex; + flex-direction: column; +`; + +export const SectionSubtitle = styled.h4` + font-size: 14px; + font-weight: 500; + color: ${props => props.theme.text.alt}; +`; + +export const SectionTitle = styled.h3` + font-size: 18px; + font-weight: 700; + color: ${props => props.theme.text.default}; + margin-bottom: 8px; +`; + +export const GrowthText = styled.h5` + color: ${props => + props.positive + ? props.theme.success.default + : props.negative ? props.theme.warn.alt : props.theme.text.alt}; + display: inline-block; + margin-right: 6px; +`; + export const Heading = styled.h1` margin-left: 16px; - font-size: 24px; + font-size: 32px; color: ${props => props.theme.text.default}; font-weight: 800; `; export const Subheading = styled.h3` margin-left: 16px; - font-size: 14px; + font-size: 16px; color: ${props => props.theme.text.alt}; font-weight: 600; + line-height: 1; + margin-bottom: 8px; `; export const StyledHeader = styled.div` @@ -24,3 +91,9 @@ export const StyledHeader = styled.div` width: 100%; align-items: center; `; + +export const HeaderText = styled.div` + display: flex; + flex-direction: column; + justify-content: space-around; +`; diff --git a/src/views/communityAnalytics/utils.js b/src/views/communityAnalytics/utils.js new file mode 100644 index 0000000000..d473b1d3ce --- /dev/null +++ b/src/views/communityAnalytics/utils.js @@ -0,0 +1,43 @@ +// @flow +import React from 'react'; +import { SectionSubtitle, GrowthText } from './style'; + +export const parseGrowth = ( + { + growth, + currentPeriodCount, + prevPeriodCount, + }: { growth: number, currentPeriodCount: number, prevPeriodCount: number }, + range: string +) => { + if (!growth) { + return null; + } else if (growth > 0) { + return ( +
+ + +{growth}% + {range} + +
+ ); + } else if (growth < 0) { + return ( +
+ + {growth}% + {range} + +
+ ); + } else { + return ( +
+ + +0% + {range} + +
+ ); + } +}; From b089e3e125c5ce122f9ada3c5d46352b690ce984 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Wed, 27 Sep 2017 10:58:07 -0700 Subject: [PATCH 04/23] Show top conversations and unanswered conversations --- iris/models/reputationEvents.js | 2 +- iris/models/thread.js | 19 +++- iris/queries/community.js | 58 +++++++++- iris/types/Community.js | 6 + src/components/listItems/index.js | 2 + src/components/viewError/style.js | 2 +- .../components/memberGrowth.js | 6 +- .../components/threadListItem.js | 67 +++++++++++ .../components/topAndNewThreads.js | 104 ++++++++++++++++++ src/views/communityAnalytics/index.js | 6 +- src/views/communityAnalytics/queries.js | 33 ++++++ src/views/communityAnalytics/style.js | 29 +++++ 12 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 src/views/communityAnalytics/components/threadListItem.js create mode 100644 src/views/communityAnalytics/components/topAndNewThreads.js diff --git a/iris/models/reputationEvents.js b/iris/models/reputationEvents.js index 1df6a9cb36..d2607ce464 100644 --- a/iris/models/reputationEvents.js +++ b/iris/models/reputationEvents.js @@ -25,7 +25,7 @@ export const getTopMembersInCommunity = ( const ac = parseInt(a.reputation, 10); return bc <= ac ? -1 : 1; }) - .slice(0, 10) + .slice(0, 20) .map(c => c.userId); return sorted; diff --git a/iris/models/thread.js b/iris/models/thread.js index d35c943af9..63df32723d 100644 --- a/iris/models/thread.js +++ b/iris/models/thread.js @@ -2,7 +2,11 @@ const { db } = require('./db'); // $FlowFixMe import { addQueue } from '../utils/workerQueue'; -const { listenToNewDocumentsIn, NEW_DOCUMENTS } = require('./utils'); +const { + listenToNewDocumentsIn, + NEW_DOCUMENTS, + parseRange, +} = require('./utils'); import { turnOffAllThreadNotifications } from '../models/usersThreads'; export const getThread = (threadId: string): Promise => { @@ -78,6 +82,19 @@ export const getThreadsByCommunity = ( .run(); }; +export const getThreadsByCommunityInTimeframe = ( + communityId: string, + range: string +): Promise> => { + const { current } = parseRange(range); + return db + .table('threads') + .getAll(communityId, { index: 'communityId' }) + .filter(db.row('createdAt').during(db.now().sub(current), db.now())) + .filter(thread => db.not(thread.hasFields('deletedAt'))) + .run(); +}; + /* When viewing a user profile we have to take two arguments into account: 1. The user who is being viewed diff --git a/iris/queries/community.js b/iris/queries/community.js index 313386f1d4..1a5835aaf0 100644 --- a/iris/queries/community.js +++ b/iris/queries/community.js @@ -18,8 +18,13 @@ const { getUserPermissionsInCommunity, getMembersInCommunity, } = require('../models/usersCommunities'); +import { getMessageCount } from '../models/message'; const { getUserByUsername } = require('../models/user'); -const { getThreadsByChannels, getThreads } = require('../models/thread'); +const { + getThreadsByChannels, + getThreads, + getThreadsByCommunityInTimeframe, +} = require('../models/thread'); const { getChannelsByCommunity, getChannelsByUserAndCommunity, @@ -385,5 +390,56 @@ module.exports = { return loaders.user.loadMany(users); }); }, + topAndNewThreads: async ( + { id }: { id: string }, + __: any, + { user, loaders }: GraphQLContext + ) => { + const currentUser = user; + + if (!currentUser) { + return new UserError('You must be signed in to continue.'); + } + + const { isOwner } = await getUserPermissionsInCommunity( + id, + currentUser.id + ); + + return getThreadsByCommunityInTimeframe( + id, + 'week' + ).then(async threads => { + if (!threads) return { topThreads: [], newThreads: [] }; + + const messageCountPromises = threads.map(async ({ id, ...thread }) => ({ + id, + messageCount: await getMessageCount(id), + })); + + // promise all the active threads and message counts + const threadsWithMessageCounts = await Promise.all( + messageCountPromises + ); + const topThreads = threadsWithMessageCounts + .filter(t => t.messageCount > 0) + .sort((a, b) => { + const bc = parseInt(b.messageCount, 10); + const ac = parseInt(a.messageCount, 10); + return bc >= ac ? -1 : 1; + }) + .slice(0, 10) + .map(t => t.id); + + const newThreads = threadsWithMessageCounts + .filter(t => t.messageCount === 0) + .map(t => t.id); + + return { + topThreads: await getThreads([...topThreads]), + newThreads: await getThreads([...newThreads]), + }; + }); + }, }, }; diff --git a/iris/types/Community.js b/iris/types/Community.js index 0101694309..a11ca5a5f6 100644 --- a/iris/types/Community.js +++ b/iris/types/Community.js @@ -84,6 +84,11 @@ const Community = /* GraphQL */ ` id: String! } + type TopAndNewThreads { + topThreads: [Thread] + newThreads: [Thread] + } + type Community { id: ID! createdAt: Date! @@ -107,6 +112,7 @@ const Community = /* GraphQL */ ` memberGrowth: GrowthData conversationGrowth: GrowthData topMembers: [User] + topAndNewThreads: TopAndNewThreads } extend type Query { diff --git a/src/components/listItems/index.js b/src/components/listItems/index.js index 9bed2e123b..f85ced9d78 100644 --- a/src/components/listItems/index.js +++ b/src/components/listItems/index.js @@ -177,6 +177,8 @@ export const UserListItem = ({ {user.contextPermissions ? ( + user.contextPermissions.reputation && + user.contextPermissions.reputation > 0 && user.contextPermissions.reputation.toLocaleString() ) : ( user.totalReputation.toLocaleString() diff --git a/src/components/viewError/style.js b/src/components/viewError/style.js index 9e21e34b43..1df9601171 100644 --- a/src/components/viewError/style.js +++ b/src/components/viewError/style.js @@ -37,5 +37,5 @@ export const Subheading = styled.h4` line-height: 1.4; color: ${props => props.theme.text.alt}; max-width: 540px; - margin-bottom: 32px; + margin-bottom: ${props => (props.small ? '16px' : '32px')}; `; diff --git a/src/views/communityAnalytics/components/memberGrowth.js b/src/views/communityAnalytics/components/memberGrowth.js index 24023033ba..958c2274d1 100644 --- a/src/views/communityAnalytics/components/memberGrowth.js +++ b/src/views/communityAnalytics/components/memberGrowth.js @@ -45,9 +45,9 @@ class MemberGrowth extends React.Component { Members {count} - {parseGrowth(weeklyGrowth, 'weekly')} - {parseGrowth(monthlyGrowth, 'monthly')} - {parseGrowth(quarterlyGrowth, 'quarterly')} + {parseGrowth(weeklyGrowth, '7 days')} + {parseGrowth(monthlyGrowth, '30 days')} + {parseGrowth(quarterlyGrowth, '90 days')} ); } diff --git a/src/views/communityAnalytics/components/threadListItem.js b/src/views/communityAnalytics/components/threadListItem.js new file mode 100644 index 0000000000..9e80b77a4e --- /dev/null +++ b/src/views/communityAnalytics/components/threadListItem.js @@ -0,0 +1,67 @@ +// @flow +import * as React from 'react'; +// $FlowFixMe +import { Link } from 'react-router-dom'; +import { convertTimestampToDate } from '../../../helpers/utils'; +import { + StyledThreadListItem, + ThreadListItemTitle, + ThreadListItemSubtitle, +} from '../style'; + +type ThreadProps = { + id: string, + creator: { + name: string, + username: string, + }, + content: { + title: string, + }, + createdAt: string, + messageCount: number, +}; + +type Props = { + thread: ThreadProps, +}; + +class ThreadListItem extends React.Component { + render() { + const { + thread: { + id, + creator: { name, username }, + content: { title }, + createdAt, + messageCount, + }, + } = this.props; + + return ( + + + + {title} + + + {messageCount > 0 && ( + + {messageCount > 1 ? `${messageCount} messages` : `1 message`} + + )} + + By {name} 路{' '} + {convertTimestampToDate(createdAt)} + + + ); + } +} + +export default ThreadListItem; diff --git a/src/views/communityAnalytics/components/topAndNewThreads.js b/src/views/communityAnalytics/components/topAndNewThreads.js new file mode 100644 index 0000000000..41cdb22b21 --- /dev/null +++ b/src/views/communityAnalytics/components/topAndNewThreads.js @@ -0,0 +1,104 @@ +// @flow +import * as React from 'react'; +// $FlowFixMe +import pure from 'recompose/pure'; +// $FlowFixMe +import compose from 'recompose/compose'; +import viewNetworkHandler from '../../../components/viewNetworkHandler'; +import { Loading } from '../../../components/loading'; +import ViewError from '../../../components/viewError'; +import ThreadListItem from './threadListItem'; +import { SectionCard, SectionSubtitle, SectionTitle } from '../style'; +import { getCommunityTopAndNewThreads } from '../queries'; + +type Thread = { + id: string, + content: { + title: string, + }, + messageCount: number, + createdAt: string, + creator: { + id: string, + username: string, + name: string, + profilePhoto: string, + }, +}; + +type Props = { + isLoading: boolean, + data: { + community: { + topAndNewThreads: { + topThreads: Array, + newThreads: Array, + }, + }, + }, +}; + +class TopAndNewThreads extends React.Component { + render() { + const { data: { community }, isLoading } = this.props; + + if (community) { + const { topAndNewThreads: { topThreads, newThreads } } = community; + // resort on the client because while the server *did* technically return the top threads, they get unsorted during the 'getThreads' model query + const sortedTopThreads = topThreads.slice().sort((a, b) => { + const bc = parseInt(b.messageCount, 10); + const ac = parseInt(a.messageCount, 10); + return bc >= ac ? -1 : 1; + }); + + return ( + + + Top this week + {sortedTopThreads.length > 0 ? ( + sortedTopThreads.map(thread => { + return ; + }) + ) : ( + + )} + + + Unanswered this week + {newThreads.length > 0 ? ( + newThreads.map(thread => { + return ; + }) + ) : ( + + )} + + + ); + } + + if (isLoading) { + return ( + + + + ); + } + + return null; + } +} + +export default compose(getCommunityTopAndNewThreads, viewNetworkHandler, pure)( + TopAndNewThreads +); diff --git a/src/views/communityAnalytics/index.js b/src/views/communityAnalytics/index.js index 90c30c2c94..c43684c3d3 100644 --- a/src/views/communityAnalytics/index.js +++ b/src/views/communityAnalytics/index.js @@ -19,6 +19,7 @@ import Header from './components/header'; import MemberGrowth from './components/memberGrowth'; import ConversationGrowth from './components/conversationGrowth'; import TopMembers from './components/topMembers'; +import TopAndNewThreads from './components/topAndNewThreads'; import { View, SectionsContainer, Column } from './style'; type Props = { @@ -67,12 +68,11 @@ class CommunitySettings extends React.Component { + - - - + diff --git a/src/views/communityAnalytics/queries.js b/src/views/communityAnalytics/queries.js index 08eeaac8df..dd186cb2da 100644 --- a/src/views/communityAnalytics/queries.js +++ b/src/views/communityAnalytics/queries.js @@ -3,6 +3,7 @@ import { graphql, gql } from 'react-apollo'; import { communityInfoFragment } from '../../api/fragments/community/communityInfo'; import { userInfoFragment } from '../../api/fragments/user/userInfo'; +import { threadInfoFragment } from '../../api/fragments/thread/threadInfo'; export const getThisCommunity = graphql( gql` @@ -136,3 +137,35 @@ export const getCommunityTopMembers = graphql( COMMUNITY_TOP_MEMBERS_QUERY, COMMUNITY_TOP_MEMBERS_OPTIONS ); + +const COMMUNITY_TOP_NEW_THREADS_QUERY = gql` + query getCommunityTopAndNewThreads($slug: String) { + community(slug: $slug) { + ...communityInfo + topAndNewThreads { + topThreads { + ...threadInfo + } + newThreads { + ...threadInfo + } + } + } + } + ${communityInfoFragment} + ${threadInfoFragment} +`; + +const COMMUNITY_TOP_NEW_THREADS_OPTIONS = { + options: ({ communitySlug }: { communitySlug: string }) => ({ + variables: { + slug: communitySlug.toLowerCase(), + }, + fetchPolicy: 'cache-and-network', + }), +}; + +export const getCommunityTopAndNewThreads = graphql( + COMMUNITY_TOP_NEW_THREADS_QUERY, + COMMUNITY_TOP_NEW_THREADS_OPTIONS +); diff --git a/src/views/communityAnalytics/style.js b/src/views/communityAnalytics/style.js index 8f4425a22e..e2ad280c57 100644 --- a/src/views/communityAnalytics/style.js +++ b/src/views/communityAnalytics/style.js @@ -16,6 +16,7 @@ export const SectionsContainer = styled.div` display: flex; flex: 1 0 auto; flex-wrap: wrap; + padding: 8px; `; export const Column = styled.div` @@ -65,6 +66,7 @@ export const GrowthText = styled.h5` : props.negative ? props.theme.warn.alt : props.theme.text.alt}; display: inline-block; margin-right: 6px; + font-size: 14px; `; export const Heading = styled.h1` @@ -97,3 +99,30 @@ export const HeaderText = styled.div` flex-direction: column; justify-content: space-around; `; + +export const StyledThreadListItem = styled.div` + display: flex; + border-bottom: 1px solid ${props => props.theme.bg.border}; + padding: 16px 0; + flex-direction: column; + + &:last-of-type { + border-bottom: 0; + } +`; +export const ThreadListItemTitle = styled.h4` + font-size: 16px; + color: ${props => props.theme.text.default}; + line-height: 1.28; +`; + +export const ThreadListItemSubtitle = styled.h5` + font-size: 14px; + color: ${props => props.theme.text.alt}; + line-height: 1.28; + margin-top: 4px; + + a:hover { + color: ${props => props.theme.text.default}; + } +`; From dda1ad0ef2e956519ef91ba658d2b7b07284a9ff Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Wed, 27 Sep 2017 11:00:58 -0700 Subject: [PATCH 05/23] Css and ordering fixes --- .../components/threadListItem.js | 20 +++++++++---------- src/views/communityAnalytics/style.js | 4 ++++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/views/communityAnalytics/components/threadListItem.js b/src/views/communityAnalytics/components/threadListItem.js index 9e80b77a4e..ed8b663eb6 100644 --- a/src/views/communityAnalytics/components/threadListItem.js +++ b/src/views/communityAnalytics/components/threadListItem.js @@ -40,23 +40,21 @@ class ThreadListItem extends React.Component { return ( - - - {title} - - + + {title} + {messageCount > 0 && ( {messageCount > 1 ? `${messageCount} messages` : `1 message`} )} - By {name} 路{' '} + By {name} 路{' '} {convertTimestampToDate(createdAt)} diff --git a/src/views/communityAnalytics/style.js b/src/views/communityAnalytics/style.js index e2ad280c57..1bc89c9668 100644 --- a/src/views/communityAnalytics/style.js +++ b/src/views/communityAnalytics/style.js @@ -114,6 +114,10 @@ export const ThreadListItemTitle = styled.h4` font-size: 16px; color: ${props => props.theme.text.default}; line-height: 1.28; + + &:hover { + color: ${props => props.theme.brand.alt}; + } `; export const ThreadListItemSubtitle = styled.h5` From c14925fdc91c3a34d9e3ebd1f5b2dbfb71f71238 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Wed, 27 Sep 2017 18:30:22 -0600 Subject: [PATCH 06/23] Fix sorting, tweaks --- src/views/communityAnalytics/components/threadListItem.js | 2 +- src/views/communityAnalytics/components/topAndNewThreads.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/communityAnalytics/components/threadListItem.js b/src/views/communityAnalytics/components/threadListItem.js index ed8b663eb6..e400cb4be6 100644 --- a/src/views/communityAnalytics/components/threadListItem.js +++ b/src/views/communityAnalytics/components/threadListItem.js @@ -18,7 +18,7 @@ type ThreadProps = { content: { title: string, }, - createdAt: string, + createdAt: Date, messageCount: number, }; diff --git a/src/views/communityAnalytics/components/topAndNewThreads.js b/src/views/communityAnalytics/components/topAndNewThreads.js index 41cdb22b21..f40236b9c1 100644 --- a/src/views/communityAnalytics/components/topAndNewThreads.js +++ b/src/views/communityAnalytics/components/topAndNewThreads.js @@ -48,7 +48,7 @@ class TopAndNewThreads extends React.Component { const sortedTopThreads = topThreads.slice().sort((a, b) => { const bc = parseInt(b.messageCount, 10); const ac = parseInt(a.messageCount, 10); - return bc >= ac ? -1 : 1; + return bc <= ac ? -1 : 1; }); return ( From b03c89adc2c208e8bb4998368da3f6bd46fe4233 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Wed, 27 Sep 2017 18:36:04 -0600 Subject: [PATCH 07/23] Fix copy --- .../communityAnalytics/components/conversationGrowth.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/communityAnalytics/components/conversationGrowth.js b/src/views/communityAnalytics/components/conversationGrowth.js index 4fa7fd7119..5cbd5cb3de 100644 --- a/src/views/communityAnalytics/components/conversationGrowth.js +++ b/src/views/communityAnalytics/components/conversationGrowth.js @@ -45,9 +45,9 @@ class ConversationGrowth extends React.Component { Conversations {count} - {parseGrowth(weeklyGrowth, 'weekly')} - {parseGrowth(monthlyGrowth, 'monthly')} - {parseGrowth(quarterlyGrowth, 'quarterly')} + {parseGrowth(weeklyGrowth, '7 days')} + {parseGrowth(monthlyGrowth, '30 days')} + {parseGrowth(quarterlyGrowth, '90 days')} ); } From 17c572f7a5a86011cf1ff42dc481460f30698a92 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 13:55:43 -0600 Subject: [PATCH 08/23] Fix flow errors --- iris/models/community.js | 60 +++++++------------ iris/types/Meta.js | 13 ---- .../components/conversationGrowth.js | 3 - .../communityAnalytics/components/header.js | 1 - .../components/memberGrowth.js | 3 - .../components/threadListItem.js | 2 - .../components/topAndNewThreads.js | 3 - .../components/topMembers.js | 3 - 8 files changed, 21 insertions(+), 67 deletions(-) diff --git a/iris/models/community.js b/iris/models/community.js index 6b55858e3c..6dedf70623 100644 --- a/iris/models/community.js +++ b/iris/models/community.js @@ -1,6 +1,5 @@ // @flow const { db } = require('./db'); -// $FlowFixMe import UserError from '../utils/UserError'; import { createChannel, deleteChannel } from './channel'; import { parseRange } from './utils'; @@ -32,7 +31,7 @@ type GetCommunityBySlugArgs = { export type GetCommunityArgs = GetCommunityByIdArgs | GetCommunityBySlugArgs; -const getCommunities = ( +export const getCommunities = ( communityIds: Array ): Promise> => { return db @@ -42,7 +41,7 @@ const getCommunities = ( .run(); }; -const getCommunitiesBySlug = ( +export const getCommunitiesBySlug = ( slugs: Array ): Promise> => { return db @@ -52,7 +51,9 @@ const getCommunitiesBySlug = ( .run(); }; -const getCommunitiesByUser = (userId: string): Promise> => { +export const getCommunitiesByUser = ( + userId: string +): Promise> => { return ( db .table('usersCommunities') @@ -77,7 +78,9 @@ const getCommunitiesByUser = (userId: string): Promise> => { ); }; -const getCommunityMetaData = (communityId: string): Promise> => { +export const getCommunityMetaData = ( + communityId: string +): Promise> => { const getChannelCount = db .table('channels') .getAll(communityId, { index: 'communityId' }) @@ -95,7 +98,7 @@ const getCommunityMetaData = (communityId: string): Promise> => { return Promise.all([getChannelCount, getMemberCount]); }; -const getMemberCount = (communityId: string): Promise => { +export const getMemberCount = (communityId: string): Promise => { return db .table('usersCommunities') .getAll(communityId, { index: 'communityId' }) @@ -130,7 +133,7 @@ export type EditCommunityArguments = { // TODO(@mxstbr): Use DBUser type type CommunityCreator = Object; -const createCommunity = ( +export const createCommunity = ( { input: { name, slug, description, website, file, coverFile }, }: CreateCommunityArguments, @@ -295,7 +298,7 @@ const createCommunity = ( }); }; -const editCommunity = ({ +export const editCommunity = ({ input: { name, slug, description, website, file, coverFile, communityId }, }: EditCommunityArguments): Promise => { return db @@ -454,7 +457,7 @@ const editCommunity = ({ - run logs for deletions over time - etc */ -const deleteCommunity = (communityId: string): Promise => { +export const deleteCommunity = (communityId: string): Promise => { return db .table('communities') .get(communityId) @@ -471,7 +474,7 @@ const deleteCommunity = (communityId: string): Promise => { .run(); }; -const setPinnedThreadInCommunity = ( +export const setPinnedThreadInCommunity = ( communityId: string, value: string ): Promise => { @@ -488,7 +491,7 @@ const setPinnedThreadInCommunity = ( .then(result => result.changes[0].new_val); }; -const unsubscribeFromAllChannelsInCommunity = ( +export const unsubscribeFromAllChannelsInCommunity = ( communityId: string, userId: string ): Promise> => { @@ -501,7 +504,7 @@ const unsubscribeFromAllChannelsInCommunity = ( }); }; -const userIsMemberOfCommunity = ( +export const userIsMemberOfCommunity = ( communityId: string, userId: string ): Promise => { @@ -514,7 +517,7 @@ const userIsMemberOfCommunity = ( }); }; -const userIsMemberOfAnyChannelInCommunity = ( +export const userIsMemberOfAnyChannelInCommunity = ( communityId: string, userId: string ): Promise => { @@ -532,7 +535,7 @@ const userIsMemberOfAnyChannelInCommunity = ( }); }; -const getTopCommunities = (amount: number): Array => { +export const getTopCommunities = (amount: number): Array => { return db .table('communities') .pluck('id') @@ -572,7 +575,7 @@ const getTopCommunities = (amount: number): Array => { }); }; -const getRecentCommunities = (amount: number): Array => { +export const getRecentCommunities = (amount: number): Array => { return db .table('communities') .orderBy({ index: db.desc('createdAt') }) @@ -581,7 +584,7 @@ const getRecentCommunities = (amount: number): Array => { .run(); }; -const getCommunitiesBySearchString = ( +export const getCommunitiesBySearchString = ( string: string ): Promise> => { return db @@ -593,7 +596,7 @@ const getCommunitiesBySearchString = ( }; // TODO(@mxstbr): Replace Array with Array -const searchThreadsInCommunity = ( +export const searchThreadsInCommunity = ( channels: Array, searchString: string ): Promise> => { @@ -606,7 +609,7 @@ const searchThreadsInCommunity = ( .run(); }; -const getThreadCount = async (communityId: string) => { +export const getThreadCount = async (communityId: string) => { return db .table('threads') .getAll(communityId, { index: 'communityId' }) @@ -646,24 +649,3 @@ export const getCommunityGrowth = async ( growth: Math.round(rate * 100), }; }; - -module.exports = { - getCommunities, - getCommunitiesBySlug, - getCommunityMetaData, - getCommunitiesByUser, - createCommunity, - editCommunity, - deleteCommunity, - setPinnedThreadInCommunity, - unsubscribeFromAllChannelsInCommunity, - userIsMemberOfCommunity, - userIsMemberOfAnyChannelInCommunity, - getTopCommunities, - getRecentCommunities, - getCommunitiesBySearchString, - searchThreadsInCommunity, - getMemberCount, - getThreadCount, - getCommunityGrowth, -}; diff --git a/iris/types/Meta.js b/iris/types/Meta.js index 95591efdc9..a4562c30f4 100644 --- a/iris/types/Meta.js +++ b/iris/types/Meta.js @@ -1,18 +1,5 @@ // @flow const Meta = /* GraphQL */ ` - type GrowthDataCounts { - growth: Float - currentPeriodCount: Int - prevPeriodCount: Int - } - - type GrowthData { - count: Int - weeklyGrowth: GrowthDataCounts - monthlyGrowth: GrowthDataCounts - quarterlyGrowth: GrowthDataCounts - } - type UsersGrowthData { count: Int dau: Int diff --git a/src/views/communityAnalytics/components/conversationGrowth.js b/src/views/communityAnalytics/components/conversationGrowth.js index 5cbd5cb3de..b7159fa5c3 100644 --- a/src/views/communityAnalytics/components/conversationGrowth.js +++ b/src/views/communityAnalytics/components/conversationGrowth.js @@ -1,8 +1,5 @@ -// @flow import * as React from 'react'; -// $FlowFixMe import pure from 'recompose/pure'; -// $FlowFixMe import compose from 'recompose/compose'; import viewNetworkHandler from '../../../components/viewNetworkHandler'; import { Loading } from '../../../components/loading'; diff --git a/src/views/communityAnalytics/components/header.js b/src/views/communityAnalytics/components/header.js index 76e0786098..391ce11b53 100644 --- a/src/views/communityAnalytics/components/header.js +++ b/src/views/communityAnalytics/components/header.js @@ -1,4 +1,3 @@ -// @flow import * as React from 'react'; import { StyledHeader, Heading, Subheading, HeaderText } from '../style'; import { Avatar } from '../../../components/avatar'; diff --git a/src/views/communityAnalytics/components/memberGrowth.js b/src/views/communityAnalytics/components/memberGrowth.js index 958c2274d1..9599679aec 100644 --- a/src/views/communityAnalytics/components/memberGrowth.js +++ b/src/views/communityAnalytics/components/memberGrowth.js @@ -1,8 +1,5 @@ -// @flow import * as React from 'react'; -// $FlowFixMe import pure from 'recompose/pure'; -// $FlowFixMe import compose from 'recompose/compose'; import viewNetworkHandler from '../../../components/viewNetworkHandler'; import { Loading } from '../../../components/loading'; diff --git a/src/views/communityAnalytics/components/threadListItem.js b/src/views/communityAnalytics/components/threadListItem.js index e400cb4be6..afc8a57e16 100644 --- a/src/views/communityAnalytics/components/threadListItem.js +++ b/src/views/communityAnalytics/components/threadListItem.js @@ -1,6 +1,4 @@ -// @flow import * as React from 'react'; -// $FlowFixMe import { Link } from 'react-router-dom'; import { convertTimestampToDate } from '../../../helpers/utils'; import { diff --git a/src/views/communityAnalytics/components/topAndNewThreads.js b/src/views/communityAnalytics/components/topAndNewThreads.js index f40236b9c1..fa8ead330f 100644 --- a/src/views/communityAnalytics/components/topAndNewThreads.js +++ b/src/views/communityAnalytics/components/topAndNewThreads.js @@ -1,8 +1,5 @@ -// @flow import * as React from 'react'; -// $FlowFixMe import pure from 'recompose/pure'; -// $FlowFixMe import compose from 'recompose/compose'; import viewNetworkHandler from '../../../components/viewNetworkHandler'; import { Loading } from '../../../components/loading'; diff --git a/src/views/communityAnalytics/components/topMembers.js b/src/views/communityAnalytics/components/topMembers.js index c71884a477..9a9ccac874 100644 --- a/src/views/communityAnalytics/components/topMembers.js +++ b/src/views/communityAnalytics/components/topMembers.js @@ -1,8 +1,5 @@ -// @flow import * as React from 'react'; -// $FlowFixMe import pure from 'recompose/pure'; -// $FlowFixMe import compose from 'recompose/compose'; import viewNetworkHandler from '../../../components/viewNetworkHandler'; import { Loading } from '../../../components/loading'; From 5578db5b31a5df9dcea26e047cbecd214b965d48 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 14:27:30 -0600 Subject: [PATCH 09/23] Revert loader and make community analytics a paid feature --- iris/models/recurringPayment.js | 2 ++ iris/queries/community.js | 8 ++++- src/views/communityAnalytics/index.js | 44 +++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/iris/models/recurringPayment.js b/iris/models/recurringPayment.js index 42cd1fea73..5b7ab4beee 100644 --- a/iris/models/recurringPayment.js +++ b/iris/models/recurringPayment.js @@ -96,9 +96,11 @@ export const getCommunityRecurringPayments = ( export const getCommunitiesRecurringPayments = ( communityIds: Array ): Promise => { + console.log('communityIds', ...communityIds); return db .table('recurringPayments') .getAll(...communityIds, { index: 'communityId' }) + .filter({ status: 'active' }) .run(); }; diff --git a/iris/queries/community.js b/iris/queries/community.js index 063822d305..7813ab0546 100644 --- a/iris/queries/community.js +++ b/iris/queries/community.js @@ -435,6 +435,12 @@ module.exports = { }); }, isPro: ({ id }: { id: string }, _: any, { loaders }: GraphQLContext) => - loaders.communityRecurringPayments.load(id), + // loaders.communityRecurringPayments.load(id), + { + return getCommunityRecurringPayments(id).then(subs => { + let filtered = subs && subs.filter(sub => sub.status === 'active'); + return !filtered || filtered.length === 0 ? false : true; + }); + }, }, }; diff --git a/src/views/communityAnalytics/index.js b/src/views/communityAnalytics/index.js index c43684c3d3..96b71b830d 100644 --- a/src/views/communityAnalytics/index.js +++ b/src/views/communityAnalytics/index.js @@ -9,6 +9,7 @@ import { connect } from 'react-redux'; // $FlowFixMe import { Link } from 'react-router-dom'; import { getThisCommunity } from './queries'; +import { openModal } from '../../actions/modals'; import { Loading } from '../../components/loading'; import AppViewWrapper from '../../components/appViewWrapper'; import ViewError from '../../components/viewError'; @@ -33,10 +34,12 @@ type Props = { name: string, profilePhoto: string, slug: string, + isPro: boolean, }, }, isLoading: boolean, hasError: boolean, + dispatch: Function, }; type State = { @@ -44,6 +47,13 @@ type State = { }; class CommunitySettings extends React.Component { + upgrade = () => { + const { dispatch, currentUser, data: { community } } = this.props; + dispatch( + openModal('COMMUNITY_UPGRADE_MODAL', { user: currentUser, community }) + ); + }; + render() { const { match: { params: { communitySlug } }, @@ -53,6 +63,30 @@ class CommunitySettings extends React.Component { } = this.props; if (community) { + if (!community.isPro) { + return ( + + + + + + + + + + ); + } + return ( { } } -export default compose(connect(), getThisCommunity, viewNetworkHandler, pure)( - CommunitySettings -); +const map = state => ({ currentUser: state.users.currentUser }); +export default compose( + connect(map), + getThisCommunity, + viewNetworkHandler, + pure +)(CommunitySettings); From ef17386a59ea1d3d6ad2388d913d9390faab3f33 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 15:30:38 -0600 Subject: [PATCH 10/23] Rework community settings routing --- src/routes.js | 2 +- src/views/communityAnalytics/index.js | 128 ++++--------- src/views/communityAnalytics/queries.js | 2 +- src/views/communityAnalytics/style.js | 4 +- .../components/header.js | 10 +- .../communitySettings/components/overview.js | 33 ++++ .../communitySettings/components/subnav.js | 31 +++ src/views/communitySettings/index.js | 178 ++++++++++-------- src/views/communitySettings/style.js | 131 +++++++++++++ 9 files changed, 344 insertions(+), 175 deletions(-) rename src/views/{communityAnalytics => communitySettings}/components/header.js (66%) create mode 100644 src/views/communitySettings/components/overview.js create mode 100644 src/views/communitySettings/components/subnav.js diff --git a/src/routes.js b/src/routes.js index 8e6b2fea60..0589fe52a5 100644 --- a/src/routes.js +++ b/src/routes.js @@ -67,7 +67,7 @@ const UserSettingsFallback = signedOutFallback(UserSettings, () => ( const CommunitySettingsFallback = signedOutFallback(CommunitySettings, () => ( )); -const CommunityAnalyticsFallback = signedOutFallback(CommunityAnalytics, () => ( +const CommunityAnalyticsFallback = signedOutFallback(CommunitySettings, () => ( )); const ChannelSettingsFallback = signedOutFallback(ChannelSettings, () => ( diff --git a/src/views/communityAnalytics/index.js b/src/views/communityAnalytics/index.js index 96b71b830d..e6486ccfa0 100644 --- a/src/views/communityAnalytics/index.js +++ b/src/views/communityAnalytics/index.js @@ -16,7 +16,6 @@ import ViewError from '../../components/viewError'; import viewNetworkHandler from '../../components/viewNetworkHandler'; import { Button, OutlineButton, ButtonRow } from '../../components/buttons'; import Titlebar from '../titlebar'; -import Header from './components/header'; import MemberGrowth from './components/memberGrowth'; import ConversationGrowth from './components/conversationGrowth'; import TopMembers from './components/topMembers'; @@ -24,19 +23,13 @@ import TopAndNewThreads from './components/topAndNewThreads'; import { View, SectionsContainer, Column } from './style'; type Props = { - match: { - params: { - communitySlug: string, - }, - }, - data: { - community: { - name: string, - profilePhoto: string, - slug: string, - isPro: boolean, - }, + community: { + name: string, + profilePhoto: string, + slug: string, + isPro: boolean, }, + communitySlug: string, isLoading: boolean, hasError: boolean, dispatch: Function, @@ -55,93 +48,54 @@ class CommunitySettings extends React.Component { }; render() { - const { - match: { params: { communitySlug } }, - data: { community }, - isLoading, - hasError, - } = this.props; + const { community, communitySlug } = this.props; if (community) { if (!community.isPro) { return ( - - - - - - - - - + + + + + ); } return ( - - - - -
- - - - - - - - - - - - - + + + + + + + + + + ); } - if (isLoading) { - return ; - } - return ( - - - - - - - Take me back - + + + + Take me back + - - - - - - + + + + + ); } } diff --git a/src/views/communityAnalytics/queries.js b/src/views/communityAnalytics/queries.js index dd186cb2da..4771783182 100644 --- a/src/views/communityAnalytics/queries.js +++ b/src/views/communityAnalytics/queries.js @@ -17,7 +17,7 @@ export const getThisCommunity = graphql( { options: props => ({ variables: { - slug: props.match.params.communitySlug.toLowerCase(), + slug: props.communitySlug.toLowerCase(), }, fetchPolicy: 'network-only', }), diff --git a/src/views/communityAnalytics/style.js b/src/views/communityAnalytics/style.js index 1bc89c9668..39ab687739 100644 --- a/src/views/communityAnalytics/style.js +++ b/src/views/communityAnalytics/style.js @@ -40,7 +40,7 @@ export const SectionCard = styled.div` border-radius: 4px; border: 1px solid ${props => props.theme.bg.border}; background: ${props => props.theme.bg.default}; - margin-bottom: 8px; + margin-bottom: 16px; padding: 16px; display: flex; flex-direction: column; @@ -80,7 +80,7 @@ export const Subheading = styled.h3` margin-left: 16px; font-size: 16px; color: ${props => props.theme.text.alt}; - font-weight: 600; + font-weight: 500; line-height: 1; margin-bottom: 8px; `; diff --git a/src/views/communityAnalytics/components/header.js b/src/views/communitySettings/components/header.js similarity index 66% rename from src/views/communityAnalytics/components/header.js rename to src/views/communitySettings/components/header.js index 391ce11b53..6942e8ef5c 100644 --- a/src/views/communityAnalytics/components/header.js +++ b/src/views/communitySettings/components/header.js @@ -1,4 +1,5 @@ import * as React from 'react'; +import { Link } from 'react-router-dom'; import { StyledHeader, Heading, Subheading, HeaderText } from '../style'; import { Avatar } from '../../../components/avatar'; @@ -6,18 +7,21 @@ type Props = { community: { name: string, profilePhoto: string, + slug: string, }, }; class Header extends React.Component { render() { - const { community: { name, profilePhoto } } = this.props; + const { community: { name, profilePhoto, slug } } = this.props; return ( - {name} - Analytics + + {name} + + Settings ); diff --git a/src/views/communitySettings/components/overview.js b/src/views/communitySettings/components/overview.js new file mode 100644 index 0000000000..36134c4140 --- /dev/null +++ b/src/views/communitySettings/components/overview.js @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { SectionsContainer, Column } from '../style'; +import { CommunityEditForm } from '../../../components/editForm'; +import RecurringPaymentsList from './recurringPaymentsList'; +import ChannelList from './channelList'; +import ImportSlack from './importSlack'; +import EmailInvites from './emailInvites'; +import Invoices from './invoices'; +import CommunityMembers from '../../../components/communityMembers'; + +class Overview extends React.Component { + render() { + const { community, communitySlug } = this.props; + + return ( + + + + + + + + + + + + + + ); + } +} + +export default Overview; diff --git a/src/views/communitySettings/components/subnav.js b/src/views/communitySettings/components/subnav.js new file mode 100644 index 0000000000..e7fa5cbb8a --- /dev/null +++ b/src/views/communitySettings/components/subnav.js @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { StyledSubnav, SubnavList, SubnavListItem } from '../style'; + +type Props = { + match: Object, + communitySlug: string, + active: boolean, +}; + +class Subnav extends React.Component { + render() { + const { communitySlug, active } = this.props; + + return ( + + + + Overview + + + + Analytics + + + + ); + } +} + +export default Subnav; diff --git a/src/views/communitySettings/index.js b/src/views/communitySettings/index.js index 8fd86b7b5a..497cadd03c 100644 --- a/src/views/communitySettings/index.js +++ b/src/views/communitySettings/index.js @@ -9,117 +9,133 @@ import { connect } from 'react-redux'; import { getThisCommunity } from './queries'; import { Loading } from '../../components/loading'; import AppViewWrapper from '../../components/appViewWrapper'; -import Column from '../../components/column'; -import ChannelList from './components/channelList'; -import ImportSlack from './components/importSlack'; -import EmailInvites from './components/emailInvites'; -import Invoices from './components/invoices'; -import RecurringPaymentsList from './components/recurringPaymentsList'; -import { CommunityEditForm } from '../../components/editForm'; -import CommunityMembers from '../../components/communityMembers'; import { Upsell404Community } from '../../components/upsell'; import viewNetworkHandler from '../../components/viewNetworkHandler'; import ViewError from '../../components/viewError'; +import Analytics from '../communityAnalytics'; +import Overview from './components/overview'; import Titlebar from '../titlebar'; +import Header from './components/header'; +import Subnav from './components/subnav'; +import { View } from './style'; -const SettingsPure = ({ - match, - history, - data: { community }, - location, - dispatch, - isLoading, - hasError, -}) => { - const communitySlug = match.params.communitySlug; +type Props = {}; + +class CommunitySettings extends React.Component { + render() { + const { + match, + history, + data: { community }, + location, + dispatch, + isLoading, + hasError, + } = this.props; + + // this is hacky, but will tell us if we're viewing analytics or the root settings view + const pathname = location.pathname; + const lastIndex = pathname.lastIndexOf('/'); + const activeRoute = pathname.substr(lastIndex + 1); + const communitySlug = match.params.communitySlug; + + if (community) { + if (!community.communityPermissions.isOwner) { + return ( + + + + + + + + ); + } + + const ActiveView = () => { + switch (activeRoute) { + case 'settings': + return ( + + ); + case 'analytics': + return ( + + ); + default: + return null; + } + }; - if (community) { - if (!community.communityPermissions.isOwner) { return ( - - - + +
+ + + + ); } - return ( - - - - - - - - - - - - - - - - ); - } + if (isLoading) { + return ; + } - if (isLoading) { - return ; - } + if (hasError) { + return ( + + + + + ); + } - if (hasError) { return ( + heading={`We weren鈥檛 able to find this community.`} + subheading={`If you want to start the ${communitySlug} community yourself, you can get started below.`} + > + + ); } +} - return ( - - - - - - - ); -}; - -const CommunitySettings = compose(getThisCommunity, viewNetworkHandler, pure)( - SettingsPure +export default compose(connect(), getThisCommunity, viewNetworkHandler, pure)( + CommunitySettings ); -export default connect()(CommunitySettings); diff --git a/src/views/communitySettings/style.js b/src/views/communitySettings/style.js index 93fb64be5b..23581bbad6 100644 --- a/src/views/communitySettings/style.js +++ b/src/views/communitySettings/style.js @@ -216,3 +216,134 @@ export const CostSubtext = styled(FlexCol)` font-size: 14px; font-weight: 500; `; + +export const View = styled.div` + display: flex; + flex-direction: column; + flex: 1; + align-self: stretch; + + @media (max-width: 768px) { + width: 100%; + } +`; + +export const SectionsContainer = styled.div` + display: flex; + flex: 1 0 auto; + flex-wrap: wrap; + padding: 8px; +`; + +export const Column = styled.div` + display: flex; + flex-direction: column; + padding: 8px; + flex: 1 0 33%; + + @media (max-width: 768px) { + flex: 1 0 100%; + padding-top: 0; + padding-bottom: 0; + + &:first-of-type { + padding-top: 8px; + } + } +`; + +export const SectionCard = styled.div` + border-radius: 4px; + border: 1px solid ${props => props.theme.bg.border}; + background: ${props => props.theme.bg.default}; + margin-bottom: 16px; + padding: 16px; + display: flex; + flex-direction: column; +`; + +export const SectionSubtitle = styled.h4` + font-size: 14px; + font-weight: 500; + color: ${props => props.theme.text.alt}; +`; + +export const SectionTitle = styled.h3` + font-size: 18px; + font-weight: 700; + color: ${props => props.theme.text.default}; + margin-bottom: 8px; +`; + +export const Heading = styled.h1` + margin-left: 16px; + font-size: 32px; + color: ${props => props.theme.text.default}; + font-weight: 800; +`; + +export const Subheading = styled.h3` + margin-left: 16px; + font-size: 16px; + color: ${props => props.theme.text.alt}; + font-weight: 500; + line-height: 1; + margin-bottom: 8px; +`; + +export const StyledHeader = styled.div` + display: flex; + padding: 32px; + border-bottom: 1px solid ${props => props.theme.bg.border}; + background: ${props => props.theme.bg.default}; + width: 100%; + align-items: center; + + @media (max-width: 768px) { + display: none; + } +`; + +export const StyledSubnav = styled.div` + display: flex; + padding: 0 32px; + border-bottom: 1px solid ${props => props.theme.bg.border}; + background: ${props => props.theme.bg.default}; + width: 100%; + align-items: center; + + @media (max-width: 768px) { + padding: 0 16px; + display: block; + } +`; + +export const SubnavList = styled.ul` + list-style-type: none; + display: flex; +`; + +export const SubnavListItem = styled.li` + position: relative; + top: 1px; + border-bottom: 1px solid + ${props => (props.active ? props.theme.text.default : 'transparent')}; + color: ${props => + props.active ? props.theme.text.default : props.theme.text.alt}; + font-weight: ${props => (props.active ? '500' : '400')}; + + &:hover { + color: ${props => props.theme.text.default}; + } + + a { + padding: 16px; + display: inline-block; + } +`; + +export const HeaderText = styled.div` + display: flex; + flex-direction: column; + justify-content: space-around; +`; From 7126e3e99dcc533330c0e513165cdf1decc2308f Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 17:26:14 -0600 Subject: [PATCH 11/23] Loading states, mobile alignment, card unification, etc --- src/components/communityMembers/style.js | 0 src/components/editForm/index.js | 7 --- src/components/editForm/style.js | 2 + .../modals/CommunityUpgradeModal/index.js | 18 +++--- .../modals/CreateChannelModal/index.js | 4 +- .../components/topMembers.js | 13 ++++- .../components/channelList.js | 50 ++++++++++------ .../components/communityMembers.js} | 44 +++++++------- .../communitySettings/components/editForm.js} | 58 +++++++++---------- .../components/emailInvites.js | 19 +++--- .../components/importSlack.js | 40 +++++++------ .../communitySettings/components/invoices.js | 18 ++++-- .../communitySettings/components/overview.js | 10 ++-- .../components/recurringPaymentsList.js | 15 +++-- .../components/upgradeCommunity.js | 13 ++--- src/views/communitySettings/style.js | 21 +++++-- 16 files changed, 188 insertions(+), 144 deletions(-) delete mode 100644 src/components/communityMembers/style.js rename src/{components/communityMembers/index.js => views/communitySettings/components/communityMembers.js} (60%) rename src/{components/editForm/community.js => views/communitySettings/components/editForm.js} (86%) diff --git a/src/components/communityMembers/style.js b/src/components/communityMembers/style.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/components/editForm/index.js b/src/components/editForm/index.js index 4e4bf2abcd..af8f7c52d7 100644 --- a/src/components/editForm/index.js +++ b/src/components/editForm/index.js @@ -6,7 +6,6 @@ import pure from 'recompose/pure'; import compose from 'recompose/compose'; import User from './user'; import Channel from './channel'; -import Community from './community'; const EditFormPure = (props: Object): React$Element => { const { type } = props; @@ -18,9 +17,6 @@ const EditFormPure = (props: Object): React$Element => { case 'channel': { return ; } - case 'community': { - return ; - } } }; @@ -35,6 +31,3 @@ export const UserEditForm = (props: FormProps) => ( export const ChannelEditForm = (props: FormProps) => ( ); -export const CommunityEditForm = (props: FormProps) => ( - -); diff --git a/src/components/editForm/style.js b/src/components/editForm/style.js index 39cfb8bb82..a35199d1ec 100644 --- a/src/components/editForm/style.js +++ b/src/components/editForm/style.js @@ -49,6 +49,8 @@ export const Actions = styled(FlexRow)` margin-top: 24px; justify-content: flex-start; flex-direction: row-reverse; + border-top: 1px solid ${props => props.theme.bg.border}; + padding-top: 16px; button + button { margin-left: 8px; diff --git a/src/components/modals/CommunityUpgradeModal/index.js b/src/components/modals/CommunityUpgradeModal/index.js index a5e756b5e3..6ac6ecf6a1 100644 --- a/src/components/modals/CommunityUpgradeModal/index.js +++ b/src/components/modals/CommunityUpgradeModal/index.js @@ -107,13 +107,7 @@ class CommunityUpgradeModal extends React.Component { > {community.isPro && ( @@ -161,10 +155,12 @@ class CommunityUpgradeModal extends React.Component { )} {!community.isPro && ( - +
+ +
)}
diff --git a/src/components/modals/CreateChannelModal/index.js b/src/components/modals/CreateChannelModal/index.js index ec43f16511..9ca0e5632e 100644 --- a/src/components/modals/CreateChannelModal/index.js +++ b/src/components/modals/CreateChannelModal/index.js @@ -306,8 +306,8 @@ class CreateChannelModal extends Component { {!modalProps.isPro && ( - Pro communities can create private channels to protect threads, - messages, and manually approve all new members. + Standard communities can create private channels to protect + threads, messages, and manually approve all new members. Learn more diff --git a/src/views/communityAnalytics/components/topMembers.js b/src/views/communityAnalytics/components/topMembers.js index 9a9ccac874..fadb686dd3 100644 --- a/src/views/communityAnalytics/components/topMembers.js +++ b/src/views/communityAnalytics/components/topMembers.js @@ -3,6 +3,7 @@ import pure from 'recompose/pure'; import compose from 'recompose/compose'; import viewNetworkHandler from '../../../components/viewNetworkHandler'; import { Loading } from '../../../components/loading'; +import ViewError from '../../../components/viewError'; import { UserListItem } from '../../../components/listItems'; import { SectionCard, SectionSubtitle, SectionTitle } from '../style'; import { getCommunityTopMembers } from '../queries'; @@ -46,7 +47,17 @@ class ConversationGrowth extends React.Component { ); } - return null; + return ( + + Top members this week + + + ); } } diff --git a/src/views/communitySettings/components/channelList.js b/src/views/communitySettings/components/channelList.js index e565a23900..e4dec483bd 100644 --- a/src/views/communitySettings/components/channelList.js +++ b/src/views/communitySettings/components/channelList.js @@ -7,13 +7,22 @@ import { connect } from 'react-redux'; //$FlowFixMe import compose from 'recompose/compose'; import { openModal } from '../../../actions/modals'; -import { LoadingCard } from '../../../components/loading'; +import { Loading } from '../../../components/loading'; import { ChannelListItem } from '../../../components/listItems'; import { IconButton, Button } from '../../../components/buttons'; import viewNetworkHandler from '../../../components/viewNetworkHandler'; import ViewError from '../../../components/viewError'; import { getCommunityChannels } from '../queries'; -import { StyledCard, ListHeading, ListContainer, ListHeader } from '../style'; +import { + StyledCard, + ListHeading, + ListContainer, + ListHeader, + SectionCard, + SectionTitle, + SectionCardFooter, + SectionSubtitle, +} from '../style'; type Props = { data: { @@ -37,17 +46,9 @@ class ChannelList extends React.Component { const channels = community.channelConnection.edges.map(c => c.node); return ( - - - Manage Channels - - + + Channels + {channels.length > 0 && channels.map(item => { @@ -67,22 +68,37 @@ class ChannelList extends React.Component { ); })} - + + + + + ); } if (isLoading) { - return ; + return ( + + + + ); } return ( - + - + ); } } diff --git a/src/components/communityMembers/index.js b/src/views/communitySettings/components/communityMembers.js similarity index 60% rename from src/components/communityMembers/index.js rename to src/views/communitySettings/components/communityMembers.js index 23a6f30d71..e321b4c462 100644 --- a/src/components/communityMembers/index.js +++ b/src/views/communitySettings/components/communityMembers.js @@ -1,22 +1,22 @@ -import React, { Component } from 'react'; -import { UserListItem } from '../listItems'; -// $FlowFixMe +import * as React from 'react'; +import { UserListItem } from '../../../components/listItems'; import pure from 'recompose/pure'; -// $FlowFixMe import compose from 'recompose/compose'; -import { LoadingCard } from '../loading'; -import ViewError from '../viewError'; -import { getCommunityMembersQuery } from '../../api/community'; -import { FetchMoreButton } from '../threadFeed/style'; +import { Loading } from '../../../components/loading'; +import ViewError from '../../../components/viewError'; +import { getCommunityMembersQuery } from '../../../api/community'; +import { FetchMoreButton } from '../../../components/threadFeed/style'; import { StyledCard, ListHeader, LargeListHeading, ListContainer, ListFooter, -} from '../listItems/style'; +} from '../../../components/listItems/style'; +import { SectionCard, SectionCardFooter, SectionTitle } from '../style'; -class CommunityMembers extends Component { +type Props = {}; +class CommunityMembers extends React.Component { render() { const { data: { error, community, networkStatus, fetchMore } } = this.props; const members = @@ -27,19 +27,21 @@ class CommunityMembers extends Component { community && community.metaData && community.metaData.members; if (networkStatus === 1) { - return ; + return ( + + + + ); } else if (error) { return ( - - - + + + ); } else { return ( - - - {totalCount} Members - + + {totalCount} Members {members && @@ -53,7 +55,7 @@ class CommunityMembers extends Component { {community.memberConnection.pageInfo.hasNextPage && ( - + Load more - + )} - + ); } } diff --git a/src/components/editForm/community.js b/src/views/communitySettings/components/editForm.js similarity index 86% rename from src/components/editForm/community.js rename to src/views/communitySettings/components/editForm.js index cb29fe5b1b..1146b1a98b 100644 --- a/src/components/editForm/community.js +++ b/src/views/communitySettings/components/editForm.js @@ -1,24 +1,19 @@ -import React, { Component } from 'react'; -//$FlowFixMe +import * as React from 'react'; import compose from 'recompose/compose'; -//$FlowFixMe import pure from 'recompose/pure'; -//$FlowFixMe import { connect } from 'react-redux'; -// $FlowFixMe import { withRouter } from 'react-router'; -// $FlowFixMe import { Link } from 'react-router-dom'; -import { track } from '../../helpers/events'; +import { track } from '../../../helpers/events'; import { editCommunityMutation, deleteCommunityMutation, -} from '../../api/community'; -import { openModal } from '../../actions/modals'; -import { addToastWithTimeout } from '../../actions/toasts'; -import { Button, TextButton, IconButton } from '../buttons'; -import { Notice } from '../listItems/style'; -import Icon from '../../components/icons'; +} from '../../../api/community'; +import { openModal } from '../../../actions/modals'; +import { addToastWithTimeout } from '../../../actions/toasts'; +import { Button, TextButton, IconButton } from '../../../components/buttons'; +import { Notice } from '../../../components/listItems/style'; +import Icon from '../../../components/icons'; import { Input, UnderlineInput, @@ -26,7 +21,7 @@ import { PhotoInput, Error, CoverInput, -} from '../formElements'; +} from '../../../components/formElements'; import { StyledCard, Form, @@ -36,9 +31,11 @@ import { TertiaryActionContainer, ImageInputWrapper, Location, -} from './style'; +} from '../../../components/editForm/style'; +import { SectionCard, SectionCardFooter, SectionTitle } from '../style'; -class CommunityWithData extends Component { +type Props = {}; +class EditForm extends React.Component { state: { name: string, slug: string, @@ -54,6 +51,7 @@ class CommunityWithData extends Component { nameError: boolean, isLoading: boolean, }; + constructor(props) { super(props); @@ -229,7 +227,7 @@ class CommunityWithData extends Component { this.props.dispatch( addToastWithTimeout( 'error', - `Something went wrong and we weren't able to save these changes. ${err}` + `Something went wrong and we weren鈥檛 able to save these changes. ${err}` ) ); }); @@ -252,7 +250,7 @@ class CommunityWithData extends Component {

{communityData.metaData.members} members will be removed from the community and the{' '} - {communityData.metaData.channels} channels you've created will + {communityData.metaData.channels} channels you鈥檝e created will be deleted.

@@ -288,23 +286,19 @@ class CommunityWithData extends Component { if (!community) { return ( - - This community doesn't exist yet. + + This community doesn鈥檛 exist yet. Want to make it? - + ); } return ( - - - - Return to Community - - Community Settings + + Community Settings

- Optional: Add your community's website + Optional: Add your community鈥檚 website @@ -375,15 +369,15 @@ class CommunityWithData extends Component { )} - + ); } } -const Community = compose( +export default compose( + connect(), deleteCommunityMutation, editCommunityMutation, withRouter, pure -)(CommunityWithData); -export default connect()(Community); +)(EditForm); diff --git a/src/views/communitySettings/components/emailInvites.js b/src/views/communitySettings/components/emailInvites.js index 69eb6dddc5..f2d4118d1c 100644 --- a/src/views/communitySettings/components/emailInvites.js +++ b/src/views/communitySettings/components/emailInvites.js @@ -22,6 +22,9 @@ import { RemoveRow, CustomMessageToggle, CustomMessageTextAreaStyles, + SectionCard, + SectionTitle, + SectionCardFooter, } from '../style'; import { StyledCard, @@ -263,11 +266,8 @@ class EmailInvites extends React.Component { } = this.state; return ( -
- Invite by Email - - Invite people to your community directly by email. - +
+ Invite members by email {contacts.map((contact, i) => { return ( @@ -284,6 +284,7 @@ class EmailInvites extends React.Component { placeholder="First name (optional)" value={contact.firstName} onChange={e => this.handleChange(e, i, 'firstName')} + hideOnMobile /> this.removeRow(i)}> @@ -324,7 +325,7 @@ class EmailInvites extends React.Component { )} - + - +
); } } const EmailInvitesCard = props => ( - + - + ); const EmailInvitesNoCard = props => ; diff --git a/src/views/communitySettings/components/importSlack.js b/src/views/communitySettings/components/importSlack.js index 7b6c85e1b3..d3ca8c88a8 100644 --- a/src/views/communitySettings/components/importSlack.js +++ b/src/views/communitySettings/components/importSlack.js @@ -21,10 +21,12 @@ import { ButtonContainer, CustomMessageToggle, CustomMessageTextAreaStyles, + SectionCard, + SectionCardFooter, + SectionTitle, } from '../style'; import { StyledCard, - LargeListHeading, Description, Notice, } from '../../../components/listItems/style'; @@ -160,31 +162,31 @@ class ImportSlack extends React.Component { if (noImport) { return (
- Invite a Slack Team + Invite a Slack Team Easily invite your team from an existing Slack team to Spectrum. Get started by connecting your team below.{' '} Note: We will not invite any of your team members - until you're ready. We will prompt for admin access to ensure that + until you鈥檙e ready. We will prompt for admin access to ensure that you own the Slack team. - + - +
); } else if (partialImport) { startPolling(5000); return (
- Inivite a Slack Team - + Inivite a Slack Team + - +
); } else if (fullImport) { @@ -196,29 +198,29 @@ class ImportSlack extends React.Component { if (hasAlreadyBeenSent) { return (
- Invite a Slack Team + Invite your Slack team This community has been connected to the{' '} {teamName} Slack team. We found {count} members with email addresses - you have already invited them to join your community. - + - +
); } else { return (
- Invite a Slack Team + Invite a Slack Team This community has been connected to the{' '} {teamName} Slack team. We found {count} members with email addresses - you can invite them to your Spectrum community in one click. - + - + { } if (isLoading) { - return ; + return ( + + + + ); } return null; @@ -277,9 +283,9 @@ class ImportSlack extends React.Component { } const ImportSlackCard = props => ( - + - + ); const ImportSlackNoCard = props => ; diff --git a/src/views/communitySettings/components/invoices.js b/src/views/communitySettings/components/invoices.js index 39a251efbc..f5282d86b1 100644 --- a/src/views/communitySettings/components/invoices.js +++ b/src/views/communitySettings/components/invoices.js @@ -7,10 +7,11 @@ import pure from 'recompose/pure'; // $FlowFixMe import { connect } from 'react-redux'; import { getCommunityInvoices } from '../../../api/community'; -import { LoadingCard } from '../../../components/loading'; +import { Loading } from '../../../components/loading'; import viewNetworkHandler from '../../../components/viewNetworkHandler'; import { InvoiceListItem } from '../../../components/listItems'; import { sortByDate } from '../../../helpers/utils'; +import { SectionCard, SectionTitle } from '../style'; import { StyledCard, LargeListHeading, @@ -42,10 +43,13 @@ class Invoices extends React.Component { if (community) { const { invoices } = community; const sortedInvoices = sortByDate(invoices.slice(), 'paidAt', 'desc'); + if (!sortedInvoices || sortedInvoices.length === 0) { + return null; + } return ( - - Payment History + + Payment History {sortedInvoices && @@ -53,12 +57,16 @@ class Invoices extends React.Component { return ; })} - + ); } if (isLoading) { - return ; + return ( + + + + ); } return null; diff --git a/src/views/communitySettings/components/overview.js b/src/views/communitySettings/components/overview.js index 36134c4140..f140798f3b 100644 --- a/src/views/communitySettings/components/overview.js +++ b/src/views/communitySettings/components/overview.js @@ -1,12 +1,12 @@ import * as React from 'react'; import { SectionsContainer, Column } from '../style'; -import { CommunityEditForm } from '../../../components/editForm'; +import EditForm from './editForm'; import RecurringPaymentsList from './recurringPaymentsList'; import ChannelList from './channelList'; import ImportSlack from './importSlack'; import EmailInvites from './emailInvites'; import Invoices from './invoices'; -import CommunityMembers from '../../../components/communityMembers'; +import CommunityMembers from './communityMembers'; class Overview extends React.Component { render() { @@ -15,15 +15,17 @@ class Overview extends React.Component { return ( - + + + + - ); diff --git a/src/views/communitySettings/components/recurringPaymentsList.js b/src/views/communitySettings/components/recurringPaymentsList.js index c56fa38b4d..17f888ee6f 100644 --- a/src/views/communitySettings/components/recurringPaymentsList.js +++ b/src/views/communitySettings/components/recurringPaymentsList.js @@ -8,6 +8,7 @@ import { IconButton } from '../../../components/buttons'; import { UpsellUpgradeCommunity } from './upgradeCommunity'; import { openModal } from '../../../actions/modals'; import { convertTimestampToDate } from '../../../helpers/utils'; +import { SectionCard, SectionTitle } from '../style'; import { StyledCard, LargeListHeading, @@ -34,10 +35,8 @@ const RecurringPaymentsList = ({ community, currentUser, dispatch }) => { if (filteredRecurringPayments.length > 0) { return ( - - - Subscriptions - + + Plan {filteredRecurringPayments.map(payment => { const amount = payment.amount / 100; @@ -56,10 +55,14 @@ const RecurringPaymentsList = ({ community, currentUser, dispatch }) => { ); })} - + ); } else { - return ; + return ( + + + + ); } }; diff --git a/src/views/communitySettings/components/upgradeCommunity.js b/src/views/communitySettings/components/upgradeCommunity.js index 65486f27fc..6abf77e1f9 100644 --- a/src/views/communitySettings/components/upgradeCommunity.js +++ b/src/views/communitySettings/components/upgradeCommunity.js @@ -18,6 +18,8 @@ import { Cost, CostNumber, CostSubtext, + SectionCard, + SectionTitle, } from '../style'; class UpsellUpgradeCommunityPure extends Component { @@ -71,8 +73,8 @@ class UpsellUpgradeCommunityPure extends Component { const { community } = this.props; return ( - - Upgrade to Spectrum Standard +
+ Upgrade to Spectrum Standard {Math.ceil(community.metaData.members / 1000) * 100} @@ -132,12 +134,7 @@ class UpsellUpgradeCommunityPure extends Component { Upgrade your community - - {/* {!upgradeError && - - {upgradeError} - } */} - +
); } } diff --git a/src/views/communitySettings/style.js b/src/views/communitySettings/style.js index 23581bbad6..9b5200a9b2 100644 --- a/src/views/communitySettings/style.js +++ b/src/views/communitySettings/style.js @@ -51,7 +51,7 @@ export const EmailInviteForm = styled.div` `; export const EmailInviteInput = styled.input` - display: flex; + display: ${props => (props.hideOnMobile ? 'none' : 'flex')}; flex: 1 1 50%; padding: 8px 12px; font-size: 14px; @@ -262,6 +262,16 @@ export const SectionCard = styled.div` flex-direction: column; `; +export const SectionCardFooter = styled.div` + border-top: 1px solid ${props => props.theme.bg.border}; + width: 100%; + padding: 16px 0 0; + margin-top: 16px; + display: flex; + align-items: center; + justify-content: flex-end; +`; + export const SectionSubtitle = styled.h4` font-size: 14px; font-weight: 500; @@ -279,14 +289,14 @@ export const Heading = styled.h1` margin-left: 16px; font-size: 32px; color: ${props => props.theme.text.default}; - font-weight: 800; + font-weight: 600; `; export const Subheading = styled.h3` margin-left: 16px; font-size: 16px; color: ${props => props.theme.text.alt}; - font-weight: 500; + font-weight: 400; line-height: 1; margin-bottom: 8px; `; @@ -298,6 +308,7 @@ export const StyledHeader = styled.div` background: ${props => props.theme.bg.default}; width: 100%; align-items: center; + flex: none; @media (max-width: 768px) { display: none; @@ -310,17 +321,19 @@ export const StyledSubnav = styled.div` border-bottom: 1px solid ${props => props.theme.bg.border}; background: ${props => props.theme.bg.default}; width: 100%; - align-items: center; + flex: none; @media (max-width: 768px) { padding: 0 16px; display: block; + justify-content: center; } `; export const SubnavList = styled.ul` list-style-type: none; display: flex; + align-items: center; `; export const SubnavListItem = styled.li` From 1edb2a506f2e8039d34ddd866631dae08bccbf04 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 17:27:38 -0600 Subject: [PATCH 12/23] Fix flow errors --- src/views/communityAnalytics/index.js | 5 ----- src/views/communitySettings/index.js | 4 ---- 2 files changed, 9 deletions(-) diff --git a/src/views/communityAnalytics/index.js b/src/views/communityAnalytics/index.js index e6486ccfa0..65e46e1bc3 100644 --- a/src/views/communityAnalytics/index.js +++ b/src/views/communityAnalytics/index.js @@ -1,12 +1,7 @@ -// @flow import React from 'react'; -// $FlowFixMe import compose from 'recompose/compose'; -//$FlowFixMe import pure from 'recompose/pure'; -// $FlowFixMe import { connect } from 'react-redux'; -// $FlowFixMe import { Link } from 'react-router-dom'; import { getThisCommunity } from './queries'; import { openModal } from '../../actions/modals'; diff --git a/src/views/communitySettings/index.js b/src/views/communitySettings/index.js index 497cadd03c..0435095148 100644 --- a/src/views/communitySettings/index.js +++ b/src/views/communitySettings/index.js @@ -1,10 +1,6 @@ -// @flow import React from 'react'; -//$FlowFixMe import compose from 'recompose/compose'; -//$FlowFixMe import pure from 'recompose/pure'; -// $FlowFixMe import { connect } from 'react-redux'; import { getThisCommunity } from './queries'; import { Loading } from '../../components/loading'; From 4daaa87a43b291bfd1b8df66d186552d6a98e4c1 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 17:29:04 -0600 Subject: [PATCH 13/23] Fix email invite input --- src/views/communitySettings/components/emailInvites.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/views/communitySettings/components/emailInvites.js b/src/views/communitySettings/components/emailInvites.js index f2d4118d1c..8b932fd55b 100644 --- a/src/views/communitySettings/components/emailInvites.js +++ b/src/views/communitySettings/components/emailInvites.js @@ -257,6 +257,7 @@ class EmailInvites extends React.Component { }; render() { + const isMobile = window.innerWidth < 768; const { contacts, isLoading, @@ -284,7 +285,7 @@ class EmailInvites extends React.Component { placeholder="First name (optional)" value={contact.firstName} onChange={e => this.handleChange(e, i, 'firstName')} - hideOnMobile + hideOnMobile={isMobile} /> this.removeRow(i)}> From 5df8617b65a27c6586f3569b57587911dde3314b Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 17:34:33 -0600 Subject: [PATCH 14/23] Typo --- src/views/communitySettings/components/importSlack.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/communitySettings/components/importSlack.js b/src/views/communitySettings/components/importSlack.js index d3ca8c88a8..ba40fd76b2 100644 --- a/src/views/communitySettings/components/importSlack.js +++ b/src/views/communitySettings/components/importSlack.js @@ -183,7 +183,7 @@ class ImportSlack extends React.Component { startPolling(5000); return (
- Inivite a Slack Team + Invite a Slack Team From fe07d51f60256bc9366624eb39e27176b5d7ebd6 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 17:50:57 -0600 Subject: [PATCH 15/23] Fix slack import card ordering --- .../components/importSlack.js | 25 ++++++++++--------- src/views/communitySettings/style.js | 3 ++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/views/communitySettings/components/importSlack.js b/src/views/communitySettings/components/importSlack.js index ba40fd76b2..6eebc2db7c 100644 --- a/src/views/communitySettings/components/importSlack.js +++ b/src/views/communitySettings/components/importSlack.js @@ -220,16 +220,6 @@ class ImportSlack extends React.Component { with email addresses - you can invite them to your Spectrum community in one click. - - - { style={{ ...CustomMessageTextAreaStyles, border: customMessageError - ? '2px solid #E3353C' - : '2px solid #DFE7EF', + ? '1px solid #E3353C' + : '1px solid #DFE7EF', }} onChange={this.handleChange} /> @@ -264,6 +254,17 @@ class ImportSlack extends React.Component { Your custom invitation message can be up to 500 characters. )} + + + +
); } diff --git a/src/views/communitySettings/style.js b/src/views/communitySettings/style.js index 9b5200a9b2..beac60accc 100644 --- a/src/views/communitySettings/style.js +++ b/src/views/communitySettings/style.js @@ -109,6 +109,7 @@ export const RemoveRow = styled.div` export const CustomMessageToggle = styled.h4` font-size: 14px; color: ${props => props.theme.text.alt}; + margin-top: 16px; &:hover { color: ${props => props.theme.brand.default}; @@ -127,7 +128,7 @@ export const CustomMessageTextAreaStyles = { borderRadius: '8px', padding: '16px', marginTop: '8px', - fontSize: '16px', + fontSize: '14px', }; export const Title = styled(H1)`font-size: 20px;`; From 5dcd4527432487485925c32c2ee4ee093d1ee32a Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 17:56:16 -0600 Subject: [PATCH 16/23] Fix Slack import loading state --- src/views/communitySettings/components/importSlack.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/views/communitySettings/components/importSlack.js b/src/views/communitySettings/components/importSlack.js index 6eebc2db7c..449ffb016d 100644 --- a/src/views/communitySettings/components/importSlack.js +++ b/src/views/communitySettings/components/importSlack.js @@ -272,11 +272,7 @@ class ImportSlack extends React.Component { } if (isLoading) { - return ( - - - - ); + return ; } return null; From 56664e84fa3e13dbefa4ef610a2ac385cc64d70b Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 18:16:21 -0600 Subject: [PATCH 17/23] Fix reputation rendering --- iris/queries/user.js | 2 +- src/components/listItems/index.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/iris/queries/user.js b/iris/queries/user.js index 42bc7f492e..e35b04efbc 100644 --- a/iris/queries/user.js +++ b/iris/queries/user.js @@ -262,7 +262,7 @@ module.exports = { isOwner, } = await getUserPermissionsInCommunity(id, user.id); return { - reputation, + reputation: reputation || 0, isModerator, isOwner, }; diff --git a/src/components/listItems/index.js b/src/components/listItems/index.js index 448d1c332b..fd3f49c744 100644 --- a/src/components/listItems/index.js +++ b/src/components/listItems/index.js @@ -178,13 +178,15 @@ export const UserListItem = ({ )} {(user.totalReputation || user.contextPermissions) && ( - + {user.contextPermissions ? ( user.contextPermissions.reputation && user.contextPermissions.reputation > 0 && user.contextPermissions.reputation.toLocaleString() - ) : ( + ) : user.totalReputation && user.totalReputation > 0 ? ( user.totalReputation.toLocaleString() + ) : ( + '0' )} )} From 2a7580865ee4a13b012b6619ec91ba660fe263ee Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 18:33:23 -0600 Subject: [PATCH 18/23] Revert "Implement release tracking in Sentry" --- .babelrc | 4 +--- .flowconfig | 1 - config-overrides.js | 5 ----- iris/routes/middlewares/raven.js | 4 ---- iris/utils/workerQueue.js | 5 +---- package.json | 3 --- shared/bull/create-queue.js | 3 --- shared/get-commit-hash.js | 11 ----------- shared/raven/index.js | 5 +---- src/helpers/events.js | 5 ----- yarn.lock | 18 ++---------------- 11 files changed, 5 insertions(+), 59 deletions(-) delete mode 100644 shared/get-commit-hash.js diff --git a/.babelrc b/.babelrc index aa5e43dd96..efc55191ee 100644 --- a/.babelrc +++ b/.babelrc @@ -20,8 +20,6 @@ "transform-flow-strip-types", "transform-object-rest-spread", "babel-plugin-transform-react-jsx", - "syntax-dynamic-import", - "preval", - "babel-macros" + "syntax-dynamic-import" ] } diff --git a/.flowconfig b/.flowconfig index 66ad0aeba1..6d8e3988f5 100644 --- a/.flowconfig +++ b/.flowconfig @@ -9,7 +9,6 @@ [options] suppress_comment=.*\\$FlowFixMe -suppress_comment=.*\\$FlowIssue esproposal.class_instance_fields=enable [lints] diff --git a/config-overrides.js b/config-overrides.js index 2e2e5e493a..8841eb1e33 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -6,7 +6,6 @@ const rewireStyledComponents = require('react-app-rewire-styled-components'); const swPrecachePlugin = require('sw-precache-webpack-plugin'); -const { injectBabelPlugin } = require('react-app-rewired'); const fs = require('fs'); const match = require('micromatch'); const WriteFilePlugin = require('write-file-webpack-plugin'); @@ -32,9 +31,5 @@ const setCustomSwPrecacheOptions = config => { module.exports = function override(config, env) { setCustomSwPrecacheOptions(config); config.plugins.push(WriteFilePlugin()); - injectBabelPlugin('babel-macros', config); - injectBabelPlugin('preval', config); - // Necessary for babel-macros to work - config.node.module = 'empty'; return rewireStyledComponents(config, env, { ssr: true }); }; diff --git a/iris/routes/middlewares/raven.js b/iris/routes/middlewares/raven.js index a0dbca1003..fbdc4cac5d 100644 --- a/iris/routes/middlewares/raven.js +++ b/iris/routes/middlewares/raven.js @@ -1,14 +1,10 @@ // @flow import Raven from 'raven'; -// $FlowIssue -import commitHash from 'shared/get-commit-hash'; Raven.config( 'https://3bd8523edd5d43d7998f9b85562d6924:d391ea04b0dc45b28610e7fad735b0d0@sentry.io/154812', { environment: process.env.NODE_ENV, - release: commitHash, - tags: { git_commit: commitHash }, } ).install(); diff --git a/iris/utils/workerQueue.js b/iris/utils/workerQueue.js index 519e8b5766..dde8f3e744 100644 --- a/iris/utils/workerQueue.js +++ b/iris/utils/workerQueue.js @@ -1,15 +1,12 @@ // @flow +// $FlowFixMe import Raven from 'raven'; -// $FlowIssue -import commitHash from 'shared/get-commit-hash'; const createQueue = require('../../shared/bull/create-queue'); if (process.env.NODE_ENV !== 'development') { Raven.config( 'https://3bd8523edd5d43d7998f9b85562d6924:d391ea04b0dc45b28610e7fad735b0d0@sentry.io/154812', { environment: process.env.NODE_ENV, - release: commitHash, - tags: { git_commit: commitHash }, } ).install(); } diff --git a/package.json b/package.json index c9e77bf490..eb76bb3d1b 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,6 @@ "private": true, "devDependencies": { "babel-cli": "^6.24.1", - "babel-macros": "^1.0.2", - "babel-plugin-preval": "^1.5.0", "babel-plugin-transform-class-properties": "^6.24.1", "cross-env": "^5.0.5", "flow-bin": "^0.55.0", @@ -106,7 +104,6 @@ "striptags": "2.x", "styled-components": "2.1.2", "subscriptions-transport-ws": "^0.7.0", - "sw-precache-webpack-plugin": "^0.11.4", "web-push": "^3.2.2" }, "scripts": { diff --git a/shared/bull/create-queue.js b/shared/bull/create-queue.js index 51f70ec1d9..4875fade5b 100644 --- a/shared/bull/create-queue.js +++ b/shared/bull/create-queue.js @@ -1,15 +1,12 @@ // @flow const Queue = require('bull'); const Raven = require('raven'); -const commitHash = require('../get-commit-hash'); if (process.env.NODE_ENV !== 'development') { Raven.config( 'https://3bd8523edd5d43d7998f9b85562d6924:d391ea04b0dc45b28610e7fad735b0d0@sentry.io/154812', { environment: process.env.NODE_ENV, - release: commitHash, - tags: { git_commit: commitHash }, } ).install(); } diff --git a/shared/get-commit-hash.js b/shared/get-commit-hash.js deleted file mode 100644 index 5fbc43f502..0000000000 --- a/shared/get-commit-hash.js +++ /dev/null @@ -1,11 +0,0 @@ -// @preval -/** - * Get the latest commit hash from this git repo, helpful for release tracking in Sentry. - * - * This file is shared between server and client. - * 鈿狅笍 DON'T PUT ANY NODE.JS OR BROWSER-SPECIFIC CODE IN HERE 鈿狅笍 - */ - -var execSync = require('child_process').execSync; -var commitHash = execSync('git rev-parse --verify HEAD').toString(); -module.exports = commitHash; diff --git a/shared/raven/index.js b/shared/raven/index.js index a0dbca1003..38d7c5332d 100644 --- a/shared/raven/index.js +++ b/shared/raven/index.js @@ -1,14 +1,11 @@ // @flow +// $FlowFixMe import Raven from 'raven'; -// $FlowIssue -import commitHash from 'shared/get-commit-hash'; Raven.config( 'https://3bd8523edd5d43d7998f9b85562d6924:d391ea04b0dc45b28610e7fad735b0d0@sentry.io/154812', { environment: process.env.NODE_ENV, - release: commitHash, - tags: { git_commit: commitHash }, } ).install(); diff --git a/src/helpers/events.js b/src/helpers/events.js index 93951a0d3f..82390113d2 100644 --- a/src/helpers/events.js +++ b/src/helpers/events.js @@ -1,12 +1,7 @@ import Raven from 'raven-js'; -// $FlowIssue -import commitHash from /* preval */ 'shared/get-commit-hash'; - Raven.config('https://3bd8523edd5d43d7998f9b85562d6924@sentry.io/154812', { whitelistUrls: [/spectrum\.chat/, /www\.spectrum\.chat/], environment: process.env.NODE_ENV, - release: commitHash, - tags: { git_commit: commitHash }, }).install(); const ga = window.ga; diff --git a/yarn.lock b/yarn.lock index 734de8e009..67e05feddc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -725,10 +725,6 @@ babel-loader@^7.1.0: loader-utils "^1.0.2" mkdirp "^0.5.1" -babel-macros@^1.0.0, babel-macros@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/babel-macros/-/babel-macros-1.0.2.tgz#04475889990243cc58a0afb5ea3308ec6b89e797" - babel-messages@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" @@ -761,16 +757,6 @@ babel-plugin-jest-hoist@^20.0.3: version "20.0.3" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz#afedc853bd3f8dc3548ea671fbe69d03cc2c1767" -babel-plugin-preval@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/babel-plugin-preval/-/babel-plugin-preval-1.5.0.tgz#be4e3353ce6ec4fd0c6b199701193306033bf54b" - dependencies: - babel-core "^6.26.0" - babel-macros "^1.0.0" - babel-register "^6.26.0" - babylon "^6.18.0" - require-from-string "^1.2.1" - babel-plugin-styled-components@^1.1.4, babel-plugin-styled-components@^1.1.7: version "1.2.0" resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.2.0.tgz#8bb8f9e69119bb8dee408c8d36a0dfef5191f3c7" @@ -7664,7 +7650,7 @@ require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" -require-from-string@^1.1.0, require-from-string@^1.2.1: +require-from-string@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" @@ -8458,7 +8444,7 @@ svgo@^0.7.0: sax "~1.2.1" whet.extend "~0.9.9" -sw-precache-webpack-plugin@0.11.4, sw-precache-webpack-plugin@^0.11.4: +sw-precache-webpack-plugin@0.11.4: version "0.11.4" resolved "https://registry.yarnpkg.com/sw-precache-webpack-plugin/-/sw-precache-webpack-plugin-0.11.4.tgz#a695017e54eed575551493a519dc1da8da2dc5e0" dependencies: From 45b9f59aa6f8be8b6d58416d957d995e9fdc40a5 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 18:34:13 -0600 Subject: [PATCH 19/23] Remove git hash on chronos --- chronos/queues/process-digest-email.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/chronos/queues/process-digest-email.js b/chronos/queues/process-digest-email.js index afb42a08c1..ec2476858b 100644 --- a/chronos/queues/process-digest-email.js +++ b/chronos/queues/process-digest-email.js @@ -1,15 +1,15 @@ // @flow const debug = require('debug')('chronos:queue:send-digest-email'); import Raven from 'raven'; -import commitHash from '../../shared/get-commit-hash'; -Raven.config( - 'https://3bd8523edd5d43d7998f9b85562d6924:d391ea04b0dc45b28610e7fad735b0d0@sentry.io/154812', - { - environment: process.env.NODE_ENV, - release: commitHash, - tags: { git_commit: commitHash }, - } -).install(); +// import commitHash from '../../shared/get-commit-hash'; +// Raven.config( +// 'https://3bd8523edd5d43d7998f9b85562d6924:d391ea04b0dc45b28610e7fad735b0d0@sentry.io/154812', +// { +// environment: process.env.NODE_ENV, +// release: commitHash, +// tags: { git_commit: commitHash }, +// } +// ).install(); import intersection from 'lodash.intersection'; import createQueue from '../../shared/bull/create-queue'; import { From 275898fbc82b02a35cb92a59d649ce0c9c44008a Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 18:50:31 -0600 Subject: [PATCH 20/23] Sorting and column max width --- .../components/conversationGrowth.js | 6 +- .../components/memberGrowth.js | 6 +- .../components/topAndNewThreads.js | 6 +- .../components/topMembers.js | 11 ++- src/views/communityAnalytics/index.js | 2 +- src/views/communityAnalytics/style.js | 68 +------------------ src/views/communityAnalytics/utils.js | 2 +- src/views/communitySettings/style.js | 12 ++++ 8 files changed, 40 insertions(+), 73 deletions(-) diff --git a/src/views/communityAnalytics/components/conversationGrowth.js b/src/views/communityAnalytics/components/conversationGrowth.js index b7159fa5c3..70b9922787 100644 --- a/src/views/communityAnalytics/components/conversationGrowth.js +++ b/src/views/communityAnalytics/components/conversationGrowth.js @@ -3,7 +3,11 @@ import pure from 'recompose/pure'; import compose from 'recompose/compose'; import viewNetworkHandler from '../../../components/viewNetworkHandler'; import { Loading } from '../../../components/loading'; -import { SectionCard, SectionSubtitle, SectionTitle } from '../style'; +import { + SectionCard, + SectionSubtitle, + SectionTitle, +} from '../../communitySettings/style'; import { getCommunityConversationGrowth } from '../queries'; import { parseGrowth } from '../utils'; diff --git a/src/views/communityAnalytics/components/memberGrowth.js b/src/views/communityAnalytics/components/memberGrowth.js index 9599679aec..7a8bdcbbf8 100644 --- a/src/views/communityAnalytics/components/memberGrowth.js +++ b/src/views/communityAnalytics/components/memberGrowth.js @@ -3,7 +3,11 @@ import pure from 'recompose/pure'; import compose from 'recompose/compose'; import viewNetworkHandler from '../../../components/viewNetworkHandler'; import { Loading } from '../../../components/loading'; -import { SectionCard, SectionSubtitle, SectionTitle } from '../style'; +import { + SectionCard, + SectionSubtitle, + SectionTitle, +} from '../../communitySettings/style'; import { getCommunityMemberGrowth } from '../queries'; import { parseGrowth } from '../utils'; diff --git a/src/views/communityAnalytics/components/topAndNewThreads.js b/src/views/communityAnalytics/components/topAndNewThreads.js index fa8ead330f..066dddb3b0 100644 --- a/src/views/communityAnalytics/components/topAndNewThreads.js +++ b/src/views/communityAnalytics/components/topAndNewThreads.js @@ -5,7 +5,11 @@ import viewNetworkHandler from '../../../components/viewNetworkHandler'; import { Loading } from '../../../components/loading'; import ViewError from '../../../components/viewError'; import ThreadListItem from './threadListItem'; -import { SectionCard, SectionSubtitle, SectionTitle } from '../style'; +import { + SectionCard, + SectionSubtitle, + SectionTitle, +} from '../../communitySettings/style'; import { getCommunityTopAndNewThreads } from '../queries'; type Thread = { diff --git a/src/views/communityAnalytics/components/topMembers.js b/src/views/communityAnalytics/components/topMembers.js index fadb686dd3..e8382d3e4b 100644 --- a/src/views/communityAnalytics/components/topMembers.js +++ b/src/views/communityAnalytics/components/topMembers.js @@ -5,7 +5,11 @@ import viewNetworkHandler from '../../../components/viewNetworkHandler'; import { Loading } from '../../../components/loading'; import ViewError from '../../../components/viewError'; import { UserListItem } from '../../../components/listItems'; -import { SectionCard, SectionSubtitle, SectionTitle } from '../style'; +import { + SectionCard, + SectionSubtitle, + SectionTitle, +} from '../../communitySettings/style'; import { getCommunityTopMembers } from '../queries'; type User = { @@ -29,6 +33,11 @@ class ConversationGrowth extends React.Component { const { data: { community }, isLoading } = this.props; if (community && community.topMembers.length > 0) { + const sortedTopMembers = community.topMembers.slice().sort((a, b) => { + const bc = parseInt(b.reputation, 10); + const ac = parseInt(a.reputation, 10); + return bc <= ac ? -1 : 1; + }); return ( Top members this week diff --git a/src/views/communityAnalytics/index.js b/src/views/communityAnalytics/index.js index 65e46e1bc3..d02e295f8e 100644 --- a/src/views/communityAnalytics/index.js +++ b/src/views/communityAnalytics/index.js @@ -15,7 +15,7 @@ import MemberGrowth from './components/memberGrowth'; import ConversationGrowth from './components/conversationGrowth'; import TopMembers from './components/topMembers'; import TopAndNewThreads from './components/topAndNewThreads'; -import { View, SectionsContainer, Column } from './style'; +import { View, SectionsContainer, Column } from '../communitySettings/style'; type Props = { community: { diff --git a/src/views/communityAnalytics/style.js b/src/views/communityAnalytics/style.js index 39ab687739..bbf30d8012 100644 --- a/src/views/communityAnalytics/style.js +++ b/src/views/communityAnalytics/style.js @@ -2,73 +2,6 @@ // $FlowFixMe import styled from 'styled-components'; -export const View = styled.div` - display: flex; - flex-direction: column; - flex: 1; - - @media (max-width: 768px) { - width: 100%; - } -`; - -export const SectionsContainer = styled.div` - display: flex; - flex: 1 0 auto; - flex-wrap: wrap; - padding: 8px; -`; - -export const Column = styled.div` - display: flex; - flex-direction: column; - padding: 8px; - flex: 1 0 33%; - - @media (max-width: 768px) { - flex: 1 0 100%; - padding-top: 0; - padding-bottom: 0; - - &:first-of-type { - padding-top: 8px; - } - } -`; - -export const SectionCard = styled.div` - border-radius: 4px; - border: 1px solid ${props => props.theme.bg.border}; - background: ${props => props.theme.bg.default}; - margin-bottom: 16px; - padding: 16px; - display: flex; - flex-direction: column; -`; - -export const SectionSubtitle = styled.h4` - font-size: 14px; - font-weight: 500; - color: ${props => props.theme.text.alt}; -`; - -export const SectionTitle = styled.h3` - font-size: 18px; - font-weight: 700; - color: ${props => props.theme.text.default}; - margin-bottom: 8px; -`; - -export const GrowthText = styled.h5` - color: ${props => - props.positive - ? props.theme.success.default - : props.negative ? props.theme.warn.alt : props.theme.text.alt}; - display: inline-block; - margin-right: 6px; - font-size: 14px; -`; - export const Heading = styled.h1` margin-left: 16px; font-size: 32px; @@ -114,6 +47,7 @@ export const ThreadListItemTitle = styled.h4` font-size: 16px; color: ${props => props.theme.text.default}; line-height: 1.28; + font-weight: 500; &:hover { color: ${props => props.theme.brand.alt}; diff --git a/src/views/communityAnalytics/utils.js b/src/views/communityAnalytics/utils.js index d473b1d3ce..f594a18700 100644 --- a/src/views/communityAnalytics/utils.js +++ b/src/views/communityAnalytics/utils.js @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import { SectionSubtitle, GrowthText } from './style'; +import { SectionSubtitle, GrowthText } from '../communitySettings/style'; export const parseGrowth = ( { diff --git a/src/views/communitySettings/style.js b/src/views/communitySettings/style.js index beac60accc..4cc1df40e0 100644 --- a/src/views/communitySettings/style.js +++ b/src/views/communitySettings/style.js @@ -234,6 +234,7 @@ export const SectionsContainer = styled.div` flex: 1 0 auto; flex-wrap: wrap; padding: 8px; + justify-content: center; `; export const Column = styled.div` @@ -241,6 +242,7 @@ export const Column = styled.div` flex-direction: column; padding: 8px; flex: 1 0 33%; + max-width: 600px; @media (max-width: 768px) { flex: 1 0 100%; @@ -361,3 +363,13 @@ export const HeaderText = styled.div` flex-direction: column; justify-content: space-around; `; + +export const GrowthText = styled.h5` + color: ${props => + props.positive + ? props.theme.success.default + : props.negative ? props.theme.warn.alt : props.theme.text.alt}; + display: inline-block; + margin-right: 6px; + font-size: 14px; +`; From ef7f0bb34b78fcd127f4c94c44aae4660858da0e Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 19:38:18 -0600 Subject: [PATCH 21/23] Fix sorting --- src/views/communityAnalytics/components/topMembers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/communityAnalytics/components/topMembers.js b/src/views/communityAnalytics/components/topMembers.js index e8382d3e4b..346101e0d4 100644 --- a/src/views/communityAnalytics/components/topMembers.js +++ b/src/views/communityAnalytics/components/topMembers.js @@ -34,8 +34,8 @@ class ConversationGrowth extends React.Component { if (community && community.topMembers.length > 0) { const sortedTopMembers = community.topMembers.slice().sort((a, b) => { - const bc = parseInt(b.reputation, 10); - const ac = parseInt(a.reputation, 10); + const bc = parseInt(b.contextPermissions.reputation, 10); + const ac = parseInt(a.contextPermissions.reputation, 10); return bc <= ac ? -1 : 1; }); return ( From 448ac1c619a98ba413d8658e9cb45a372ad39543 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 20:19:43 -0600 Subject: [PATCH 22/23] Actually fix sorting, polish --- iris/queries/community.js | 3 +- src/components/listItems/index.js | 2 +- src/components/listItems/style.js | 19 ++++++++-- .../components/conversationGrowth.js | 12 +++--- .../components/memberGrowth.js | 10 ++--- .../components/threadListItem.js | 3 +- .../components/topAndNewThreads.js | 4 +- .../components/topMembers.js | 38 +++++++++++++++---- src/views/communityAnalytics/style.js | 1 + src/views/communitySettings/style.js | 8 +++- 10 files changed, 72 insertions(+), 28 deletions(-) diff --git a/iris/queries/community.js b/iris/queries/community.js index 7813ab0546..31b5e18843 100644 --- a/iris/queries/community.js +++ b/iris/queries/community.js @@ -414,12 +414,13 @@ module.exports = { const threadsWithMessageCounts = await Promise.all( messageCountPromises ); + const topThreads = threadsWithMessageCounts .filter(t => t.messageCount > 0) .sort((a, b) => { const bc = parseInt(b.messageCount, 10); const ac = parseInt(a.messageCount, 10); - return bc >= ac ? -1 : 1; + return bc <= ac ? -1 : 1; }) .slice(0, 10) .map(t => t.id); diff --git a/src/components/listItems/index.js b/src/components/listItems/index.js index fd3f49c744..8c5471ba3d 100644 --- a/src/components/listItems/index.js +++ b/src/components/listItems/index.js @@ -154,7 +154,7 @@ export const UserListItem = ({ children, }: Object): React$Element => { return ( - + (props.border ? props.theme.bg.border : 'transparent')}; + + &:first-of-type { + border-top: 0; + } + + &:last-of-type { + padding-bottom: 0; + } &:hover > div > div h3, &:hover .action { @@ -49,14 +59,15 @@ export const Row = styled(FlexRow)` `; export const Heading = styled(H3)` - font-weight: 700; + font-weight: 500; transition: ${Transition.hover.off}; line-height: 1.2; + margin-top: 6px; `; export const Meta = styled(H4)` - font-size: 12px; - font-weight: 500; + font-size: 14px; + font-weight: 400; color: ${({ theme }) => theme.text.alt}; a { diff --git a/src/views/communityAnalytics/components/conversationGrowth.js b/src/views/communityAnalytics/components/conversationGrowth.js index 70b9922787..cf0bf9dbfd 100644 --- a/src/views/communityAnalytics/components/conversationGrowth.js +++ b/src/views/communityAnalytics/components/conversationGrowth.js @@ -44,11 +44,13 @@ class ConversationGrowth extends React.Component { } = community.conversationGrowth; return ( - Conversations - {count} - {parseGrowth(weeklyGrowth, '7 days')} - {parseGrowth(monthlyGrowth, '30 days')} - {parseGrowth(quarterlyGrowth, '90 days')} + Your community鈥榮 conversations + + {count.toLocaleString()} total conversations + + {parseGrowth(weeklyGrowth, 'this week')} + {parseGrowth(monthlyGrowth, 'this month')} + {parseGrowth(quarterlyGrowth, 'this quarter')} ); } diff --git a/src/views/communityAnalytics/components/memberGrowth.js b/src/views/communityAnalytics/components/memberGrowth.js index 7a8bdcbbf8..077d3a9192 100644 --- a/src/views/communityAnalytics/components/memberGrowth.js +++ b/src/views/communityAnalytics/components/memberGrowth.js @@ -44,11 +44,11 @@ class MemberGrowth extends React.Component { } = community.memberGrowth; return ( - Members - {count} - {parseGrowth(weeklyGrowth, '7 days')} - {parseGrowth(monthlyGrowth, '30 days')} - {parseGrowth(quarterlyGrowth, '90 days')} + Your community + {count.toLocaleString()} members + {parseGrowth(weeklyGrowth, 'this week')} + {parseGrowth(monthlyGrowth, 'this month')} + {parseGrowth(quarterlyGrowth, 'this quarter')} ); } diff --git a/src/views/communityAnalytics/components/threadListItem.js b/src/views/communityAnalytics/components/threadListItem.js index afc8a57e16..7b416101c0 100644 --- a/src/views/communityAnalytics/components/threadListItem.js +++ b/src/views/communityAnalytics/components/threadListItem.js @@ -52,8 +52,7 @@ class ThreadListItem extends React.Component { )} - By {name} 路{' '} - {convertTimestampToDate(createdAt)} + By {name} ); diff --git a/src/views/communityAnalytics/components/topAndNewThreads.js b/src/views/communityAnalytics/components/topAndNewThreads.js index 066dddb3b0..f18f34db59 100644 --- a/src/views/communityAnalytics/components/topAndNewThreads.js +++ b/src/views/communityAnalytics/components/topAndNewThreads.js @@ -55,7 +55,7 @@ class TopAndNewThreads extends React.Component { return ( - Top this week + Top conversations this week {sortedTopThreads.length > 0 ? ( sortedTopThreads.map(thread => { return ; @@ -70,7 +70,7 @@ class TopAndNewThreads extends React.Component { )} - Unanswered this week + Unanswered conversations this week {newThreads.length > 0 ? ( newThreads.map(thread => { return ; diff --git a/src/views/communityAnalytics/components/topMembers.js b/src/views/communityAnalytics/components/topMembers.js index 346101e0d4..8647a7e6f5 100644 --- a/src/views/communityAnalytics/components/topMembers.js +++ b/src/views/communityAnalytics/components/topMembers.js @@ -1,5 +1,9 @@ import * as React from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; import pure from 'recompose/pure'; +import Icon from '../../../components/icons'; +import { initNewThreadWithUser } from '../../../actions/directMessageThreads'; import compose from 'recompose/compose'; import viewNetworkHandler from '../../../components/viewNetworkHandler'; import { Loading } from '../../../components/loading'; @@ -9,6 +13,7 @@ import { SectionCard, SectionSubtitle, SectionTitle, + MessageIcon, } from '../../communitySettings/style'; import { getCommunityTopMembers } from '../queries'; @@ -29,6 +34,11 @@ type Props = { }; class ConversationGrowth extends React.Component { + initMessage = user => { + this.props.dispatch(initNewThreadWithUser(user)); + this.props.history.push('/messages/new'); + }; + render() { const { data: { community }, isLoading } = this.props; @@ -40,9 +50,19 @@ class ConversationGrowth extends React.Component { }); return ( - Top members this week - {community.topMembers.map(user => { - return ; + Top members this week + {sortedTopMembers.map(user => { + return ( + + this.initMessage(user)} + > + + + + ); })} ); @@ -58,7 +78,7 @@ class ConversationGrowth extends React.Component { return ( - Top members this week + Top members this week { } } -export default compose(getCommunityTopMembers, viewNetworkHandler, pure)( - ConversationGrowth -); +export default compose( + connect(), + withRouter, + getCommunityTopMembers, + viewNetworkHandler, + pure +)(ConversationGrowth); diff --git a/src/views/communityAnalytics/style.js b/src/views/communityAnalytics/style.js index bbf30d8012..3a9583d8f2 100644 --- a/src/views/communityAnalytics/style.js +++ b/src/views/communityAnalytics/style.js @@ -41,6 +41,7 @@ export const StyledThreadListItem = styled.div` &:last-of-type { border-bottom: 0; + padding-bottom: 0; } `; export const ThreadListItemTitle = styled.h4` diff --git a/src/views/communitySettings/style.js b/src/views/communitySettings/style.js index 4cc1df40e0..56e0fc5d01 100644 --- a/src/views/communitySettings/style.js +++ b/src/views/communitySettings/style.js @@ -1,7 +1,7 @@ import styled from 'styled-components'; import Card from '../../components/card'; import { Link } from 'react-router-dom'; -import { FlexCol, H1, H2, H3, Span } from '../../components/globals'; +import { FlexCol, H1, H2, H3, Span, Tooltip } from '../../components/globals'; export const ListHeader = styled.div` display: flex; @@ -373,3 +373,9 @@ export const GrowthText = styled.h5` margin-right: 6px; font-size: 14px; `; + +export const MessageIcon = styled.div` + color: ${props => props.theme.brand.alt}; + cursor: pointer; + ${Tooltip} top: 2px; +`; From 8e99ed6d2f99258e2d8c324f3407d74b87645f7c Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 3 Oct 2017 20:27:04 -0600 Subject: [PATCH 23/23] Small fixes --- src/views/communitySettings/components/communityMembers.js | 2 +- src/views/communitySettings/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/communitySettings/components/communityMembers.js b/src/views/communitySettings/components/communityMembers.js index e321b4c462..e6e21660f0 100644 --- a/src/views/communitySettings/components/communityMembers.js +++ b/src/views/communitySettings/components/communityMembers.js @@ -41,7 +41,7 @@ class CommunityMembers extends React.Component { } else { return ( - {totalCount} Members + {totalCount.toLocaleString()} Members {members && diff --git a/src/views/communitySettings/index.js b/src/views/communitySettings/index.js index 0435095148..4e2e4dab04 100644 --- a/src/views/communitySettings/index.js +++ b/src/views/communitySettings/index.js @@ -47,7 +47,7 @@ class CommunitySettings extends React.Component { />