Territory analytics (#1926)
* add territory to analytics selectors * implement territory analytics, revert user satistics header * fix linting errors * disallow some territory names * fix linting error * minor adjustments to header * escape input * 404 on non-existant sub * exclude unused queries depending on sub select --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
parent
5de9d92af2
commit
73170ba8a2
@ -121,6 +121,39 @@ export default {
|
|||||||
FROM ${viewGroup(range, 'stacking_growth')}
|
FROM ${viewGroup(range, 'stacking_growth')}
|
||||||
GROUP BY time
|
GROUP BY time
|
||||||
ORDER BY time ASC`, ...range)
|
ORDER BY time ASC`, ...range)
|
||||||
|
},
|
||||||
|
itemGrowthSubs: async (parent, { when, to, from, sub }, { models }) => {
|
||||||
|
const range = whenRange(when, from, to)
|
||||||
|
|
||||||
|
const subExists = await models.sub.findUnique({ where: { name: sub } })
|
||||||
|
if (!subExists) throw new Error('Sub not found')
|
||||||
|
|
||||||
|
return await models.$queryRawUnsafe(`
|
||||||
|
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
|
||||||
|
json_build_object('name', 'posts', 'value', coalesce(sum(posts),0)),
|
||||||
|
json_build_object('name', 'comments', 'value', coalesce(sum(comments),0))
|
||||||
|
) AS data
|
||||||
|
FROM ${viewGroup(range, 'sub_stats')}
|
||||||
|
WHERE sub_name = $3
|
||||||
|
GROUP BY time
|
||||||
|
ORDER BY time ASC`, ...range, sub)
|
||||||
|
},
|
||||||
|
revenueGrowthSubs: async (parent, { when, to, from, sub }, { models }) => {
|
||||||
|
const range = whenRange(when, from, to)
|
||||||
|
|
||||||
|
const subExists = await models.sub.findUnique({ where: { name: sub } })
|
||||||
|
if (!subExists) throw new Error('Sub not found')
|
||||||
|
|
||||||
|
return await models.$queryRawUnsafe(`
|
||||||
|
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
|
||||||
|
json_build_object('name', 'revenue', 'value', coalesce(sum(msats_revenue/1000),0)),
|
||||||
|
json_build_object('name', 'stacking', 'value', coalesce(sum(msats_stacked/1000),0)),
|
||||||
|
json_build_object('name', 'spending', 'value', coalesce(sum(msats_spent/1000),0))
|
||||||
|
) AS data
|
||||||
|
FROM ${viewGroup(range, 'sub_stats')}
|
||||||
|
WHERE sub_name = $3
|
||||||
|
GROUP BY time
|
||||||
|
ORDER BY time ASC`, ...range, sub)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ export default gql`
|
|||||||
spenderGrowth(when: String, from: String, to: String): [TimeData!]!
|
spenderGrowth(when: String, from: String, to: String): [TimeData!]!
|
||||||
stackingGrowth(when: String, from: String, to: String): [TimeData!]!
|
stackingGrowth(when: String, from: String, to: String): [TimeData!]!
|
||||||
stackerGrowth(when: String, from: String, to: String): [TimeData!]!
|
stackerGrowth(when: String, from: String, to: String): [TimeData!]!
|
||||||
|
itemGrowthSubs(when: String, from: String, to: String, sub: String): [TimeData!]!
|
||||||
|
revenueGrowthSubs(when: String, from: String, to: String, sub: String): [TimeData!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimeData {
|
type TimeData {
|
||||||
|
@ -173,7 +173,7 @@ export default function Footer ({ links = true }) {
|
|||||||
<Rewards />
|
<Rewards />
|
||||||
</div>
|
</div>
|
||||||
<div className='mb-0' style={{ fontWeight: 500 }}>
|
<div className='mb-0' style={{ fontWeight: 500 }}>
|
||||||
<Link href='/stackers/day' className='nav-link p-0 p-0 d-inline-flex'>
|
<Link href='/stackers/all/day' className='nav-link p-0 p-0 d-inline-flex'>
|
||||||
analytics
|
analytics
|
||||||
</Link>
|
</Link>
|
||||||
<span className='mx-2 text-muted'> \ </span>
|
<span className='mx-2 text-muted'> \ </span>
|
||||||
|
79
components/sub-analytics-header.js
Normal file
79
components/sub-analytics-header.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { Select, DatePicker } from './form'
|
||||||
|
import { useSubs } from './sub-select'
|
||||||
|
import { WHENS } from '@/lib/constants'
|
||||||
|
import { whenToFrom } from '@/lib/time'
|
||||||
|
import styles from './sub-select.module.css'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
export function SubAnalyticsHeader ({ pathname = null }) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const path = pathname || 'stackers'
|
||||||
|
|
||||||
|
const select = async values => {
|
||||||
|
const { sub, when, ...query } = values
|
||||||
|
|
||||||
|
if (when !== 'custom') { delete query.from; delete query.to }
|
||||||
|
if (query.from && !query.to) return
|
||||||
|
|
||||||
|
await router.push({
|
||||||
|
|
||||||
|
pathname: `/${path}/${sub}/${when}`,
|
||||||
|
query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const when = router.query.when || 'day'
|
||||||
|
const sub = router.query.sub || 'all'
|
||||||
|
|
||||||
|
const subs = useSubs({ prependSubs: ['all'], sub, appendSubs: [], filterSubs: () => true })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='text-muted fw-bold my-0 d-flex align-items-center flex-wrap'>
|
||||||
|
<div className='text-muted fw-bold mb-2 d-flex align-items-center'>
|
||||||
|
stacker analytics in
|
||||||
|
<Select
|
||||||
|
groupClassName='mb-0 mx-2'
|
||||||
|
className={classNames(styles.subSelect, styles.subSelectSmall)}
|
||||||
|
name='sub'
|
||||||
|
size='sm'
|
||||||
|
items={subs}
|
||||||
|
value={sub}
|
||||||
|
noForm
|
||||||
|
onChange={(formik, e) => {
|
||||||
|
const range = when === 'custom' ? { from: router.query.from, to: router.query.to } : {}
|
||||||
|
select({ sub: e.target.value, when, ...range })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
for
|
||||||
|
<Select
|
||||||
|
groupClassName='mb-0 mx-2'
|
||||||
|
className='w-auto'
|
||||||
|
name='when'
|
||||||
|
size='sm'
|
||||||
|
items={WHENS}
|
||||||
|
value={when}
|
||||||
|
noForm
|
||||||
|
onChange={(formik, e) => {
|
||||||
|
const range = e.target.value === 'custom' ? { from: whenToFrom(when), to: Date.now() } : {}
|
||||||
|
select({ sub, when: e.target.value, ...range })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{when === 'custom' &&
|
||||||
|
<DatePicker
|
||||||
|
noForm
|
||||||
|
fromName='from'
|
||||||
|
toName='to'
|
||||||
|
className='p-0 px-2 mb-0'
|
||||||
|
onChange={(formik, [from, to], e) => {
|
||||||
|
select({ sub, when, from: from.getTime(), to: to.getTime() })
|
||||||
|
}}
|
||||||
|
from={router.query.from}
|
||||||
|
to={router.query.to}
|
||||||
|
when={when}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -3,10 +3,10 @@ import { Select, DatePicker } from './form'
|
|||||||
import { WHENS } from '@/lib/constants'
|
import { WHENS } from '@/lib/constants'
|
||||||
import { whenToFrom } from '@/lib/time'
|
import { whenToFrom } from '@/lib/time'
|
||||||
|
|
||||||
export function UsageHeader ({ pathname = null }) {
|
export function UserAnalyticsHeader ({ pathname = null }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const path = pathname || 'stackers'
|
const path = pathname || 'satistics/graph'
|
||||||
|
|
||||||
const select = async values => {
|
const select = async values => {
|
||||||
const { when, ...query } = values
|
const { when, ...query } = values
|
@ -2,6 +2,7 @@
|
|||||||
// to be loaded from the server
|
// to be loaded from the server
|
||||||
export const DEFAULT_SUBS = ['bitcoin', 'nostr', 'tech', 'meta', 'jobs']
|
export const DEFAULT_SUBS = ['bitcoin', 'nostr', 'tech', 'meta', 'jobs']
|
||||||
export const DEFAULT_SUBS_NO_JOBS = DEFAULT_SUBS.filter(s => s !== 'jobs')
|
export const DEFAULT_SUBS_NO_JOBS = DEFAULT_SUBS.filter(s => s !== 'jobs')
|
||||||
|
export const RESERVED_SUB_NAMES = ['all', 'home']
|
||||||
|
|
||||||
export const PAID_ACTION_PAYMENT_METHODS = {
|
export const PAID_ACTION_PAYMENT_METHODS = {
|
||||||
FEE_CREDIT: 'FEE_CREDIT',
|
FEE_CREDIT: 'FEE_CREDIT',
|
||||||
|
@ -2,7 +2,8 @@ import { string, ValidationError, number, object, array, boolean, date } from '.
|
|||||||
import {
|
import {
|
||||||
BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES,
|
BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES,
|
||||||
MIN_POLL_NUM_CHOICES, MAX_FORWARDS, BOOST_MULT, MAX_TERRITORY_DESC_LENGTH, POST_TYPES,
|
MIN_POLL_NUM_CHOICES, MAX_FORWARDS, BOOST_MULT, MAX_TERRITORY_DESC_LENGTH, POST_TYPES,
|
||||||
TERRITORY_BILLING_TYPES, MAX_COMMENT_TEXT_LENGTH, MAX_POST_TEXT_LENGTH, MIN_TITLE_LENGTH, BOUNTY_MIN, BOUNTY_MAX
|
TERRITORY_BILLING_TYPES, MAX_COMMENT_TEXT_LENGTH, MAX_POST_TEXT_LENGTH, MIN_TITLE_LENGTH, BOUNTY_MIN, BOUNTY_MAX,
|
||||||
|
RESERVED_SUB_NAMES
|
||||||
} from './constants'
|
} from './constants'
|
||||||
import { SUPPORTED_CURRENCIES } from './currency'
|
import { SUPPORTED_CURRENCIES } from './currency'
|
||||||
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
|
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
|
||||||
@ -306,7 +307,7 @@ export function territorySchema (args) {
|
|||||||
const isArchived = sub => sub.status === 'STOPPED'
|
const isArchived = sub => sub.status === 'STOPPED'
|
||||||
const filter = sub => editing ? !isEdit(sub) : !isArchived(sub)
|
const filter = sub => editing ? !isEdit(sub) : !isArchived(sub)
|
||||||
const exists = await subExists(name, { ...args, filter })
|
const exists = await subExists(name, { ...args, filter })
|
||||||
return !exists
|
return !exists & !RESERVED_SUB_NAMES.includes(name)
|
||||||
},
|
},
|
||||||
message: 'taken'
|
message: 'taken'
|
||||||
}),
|
}),
|
||||||
|
@ -6,7 +6,7 @@ import { useRouter } from 'next/router'
|
|||||||
import PageLoading from '@/components/page-loading'
|
import PageLoading from '@/components/page-loading'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { numWithUnits } from '@/lib/format'
|
import { numWithUnits } from '@/lib/format'
|
||||||
import { UsageHeader } from '@/components/usage-header'
|
import { UserAnalyticsHeader } from '@/components/user-analytics-header'
|
||||||
import { SatisticsHeader } from '..'
|
import { SatisticsHeader } from '..'
|
||||||
import { WhenComposedChartSkeleton, WhenAreaChartSkeleton } from '@/components/charts-skeletons'
|
import { WhenComposedChartSkeleton, WhenAreaChartSkeleton } from '@/components/charts-skeletons'
|
||||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
|
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
|
||||||
@ -55,7 +55,7 @@ export default function Satistics ({ ssrData }) {
|
|||||||
<SatisticsHeader />
|
<SatisticsHeader />
|
||||||
<div className='tab-content' id='myTabContent'>
|
<div className='tab-content' id='myTabContent'>
|
||||||
<div className='tab-pane fade show active text-muted' id='statistics' role='tabpanel' aria-labelledby='statistics-tab'>
|
<div className='tab-pane fade show active text-muted' id='statistics' role='tabpanel' aria-labelledby='statistics-tab'>
|
||||||
<UsageHeader pathname='satistics/graphs' />
|
<UserAnalyticsHeader pathname='satistics/graphs' />
|
||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
<div className='d-flex row justify-content-between'>
|
<div className='d-flex row justify-content-between'>
|
||||||
<div className='col-md-6 mb-2'>
|
<div className='col-md-6 mb-2'>
|
||||||
|
157
pages/stackers/[sub]/[when].js
Normal file
157
pages/stackers/[sub]/[when].js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { gql, useQuery } from '@apollo/client'
|
||||||
|
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||||
|
import Layout from '@/components/layout'
|
||||||
|
import Col from 'react-bootstrap/Col'
|
||||||
|
import Row from 'react-bootstrap/Row'
|
||||||
|
import { SubAnalyticsHeader } from '@/components/sub-analytics-header'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
import PageLoading from '@/components/page-loading'
|
||||||
|
import { WhenAreaChartSkeleton, WhenComposedChartSkeleton, WhenLineChartSkeleton } from '@/components/charts-skeletons'
|
||||||
|
|
||||||
|
const WhenAreaChart = dynamic(() => import('@/components/charts').then(mod => mod.WhenAreaChart), {
|
||||||
|
loading: () => <WhenAreaChartSkeleton />
|
||||||
|
})
|
||||||
|
const WhenLineChart = dynamic(() => import('@/components/charts').then(mod => mod.WhenLineChart), {
|
||||||
|
loading: () => <WhenLineChartSkeleton />
|
||||||
|
})
|
||||||
|
const WhenComposedChart = dynamic(() => import('@/components/charts').then(mod => mod.WhenComposedChart), {
|
||||||
|
loading: () => <WhenComposedChartSkeleton />
|
||||||
|
})
|
||||||
|
|
||||||
|
const GROWTH_QUERY = gql`
|
||||||
|
query Growth($when: String!, $from: String, $to: String, $sub: String, $subSelect: Boolean = false)
|
||||||
|
{
|
||||||
|
registrationGrowth(when: $when, from: $from, to: $to) @skip(if: $subSelect) {
|
||||||
|
time
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemGrowth(when: $when, from: $from, to: $to) @skip(if: $subSelect) {
|
||||||
|
time
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spendingGrowth(when: $when, from: $from, to: $to) @skip(if: $subSelect) {
|
||||||
|
time
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spenderGrowth(when: $when, from: $from, to: $to) @skip(if: $subSelect) {
|
||||||
|
time
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stackingGrowth(when: $when, from: $from, to: $to) @skip(if: $subSelect) {
|
||||||
|
time
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stackerGrowth(when: $when, from: $from, to: $to) @skip(if: $subSelect) {
|
||||||
|
time
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemGrowthSubs(when: $when, from: $from, to: $to, sub: $sub) @include(if: $subSelect) {
|
||||||
|
time
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
revenueGrowthSubs(when: $when, from: $from, to: $to, sub: $sub) @include(if: $subSelect) {
|
||||||
|
time
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
const variablesFunc = vars => ({ ...vars, subSelect: vars.sub !== 'all' })
|
||||||
|
export const getServerSideProps = getGetServerSideProps({ query: GROWTH_QUERY, variables: variablesFunc })
|
||||||
|
|
||||||
|
export default function Growth ({ ssrData }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const { when, from, to, sub } = router.query
|
||||||
|
|
||||||
|
const { data } = useQuery(GROWTH_QUERY, { variables: { when, from, to, sub, subSelect: sub !== 'all' } })
|
||||||
|
if (!data && !ssrData) return <PageLoading />
|
||||||
|
|
||||||
|
const {
|
||||||
|
registrationGrowth,
|
||||||
|
itemGrowth,
|
||||||
|
spendingGrowth,
|
||||||
|
spenderGrowth,
|
||||||
|
stackingGrowth,
|
||||||
|
stackerGrowth,
|
||||||
|
itemGrowthSubs,
|
||||||
|
revenueGrowthSubs
|
||||||
|
} = data || ssrData
|
||||||
|
|
||||||
|
if (sub === 'all') {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<SubAnalyticsHeader />
|
||||||
|
<Row>
|
||||||
|
<Col className='mt-3'>
|
||||||
|
<div className='text-center text-muted fw-bold'>stackers</div>
|
||||||
|
<WhenLineChart data={stackerGrowth} />
|
||||||
|
</Col>
|
||||||
|
<Col className='mt-3'>
|
||||||
|
<div className='text-center text-muted fw-bold'>stacking</div>
|
||||||
|
<WhenAreaChart data={stackingGrowth} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col className='mt-3'>
|
||||||
|
<div className='text-center text-muted fw-bold'>spenders</div>
|
||||||
|
<WhenLineChart data={spenderGrowth} />
|
||||||
|
</Col>
|
||||||
|
<Col className='mt-3'>
|
||||||
|
<div className='text-center text-muted fw-bold'>spending</div>
|
||||||
|
<WhenAreaChart data={spendingGrowth} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col className='mt-3'>
|
||||||
|
<div className='text-center text-muted fw-bold'>registrations</div>
|
||||||
|
<WhenAreaChart data={registrationGrowth} />
|
||||||
|
</Col>
|
||||||
|
<Col className='mt-3'>
|
||||||
|
<div className='text-center text-muted fw-bold'>items</div>
|
||||||
|
<WhenComposedChart data={itemGrowth} areaNames={['posts', 'comments', 'jobs']} areaAxis='left' lineNames={['comments/posts', 'territories']} lineAxis='right' barNames={['zaps']} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<SubAnalyticsHeader />
|
||||||
|
<Row>
|
||||||
|
<Col className='mt-3'>
|
||||||
|
<div className='text-center text-muted fw-bold'>items</div>
|
||||||
|
<WhenLineChart data={itemGrowthSubs} />
|
||||||
|
</Col>
|
||||||
|
<Col className='mt-3'>
|
||||||
|
<div className='text-center text-muted fw-bold'>sats</div>
|
||||||
|
<WhenLineChart data={revenueGrowthSubs} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,115 +0,0 @@
|
|||||||
import { gql, useQuery } from '@apollo/client'
|
|
||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
|
||||||
import Layout from '@/components/layout'
|
|
||||||
import Col from 'react-bootstrap/Col'
|
|
||||||
import Row from 'react-bootstrap/Row'
|
|
||||||
import { UsageHeader } from '@/components/usage-header'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import dynamic from 'next/dynamic'
|
|
||||||
import PageLoading from '@/components/page-loading'
|
|
||||||
import { WhenAreaChartSkeleton, WhenComposedChartSkeleton, WhenLineChartSkeleton } from '@/components/charts-skeletons'
|
|
||||||
|
|
||||||
const WhenAreaChart = dynamic(() => import('@/components/charts').then(mod => mod.WhenAreaChart), {
|
|
||||||
loading: () => <WhenAreaChartSkeleton />
|
|
||||||
})
|
|
||||||
const WhenLineChart = dynamic(() => import('@/components/charts').then(mod => mod.WhenLineChart), {
|
|
||||||
loading: () => <WhenLineChartSkeleton />
|
|
||||||
})
|
|
||||||
const WhenComposedChart = dynamic(() => import('@/components/charts').then(mod => mod.WhenComposedChart), {
|
|
||||||
loading: () => <WhenComposedChartSkeleton />
|
|
||||||
})
|
|
||||||
|
|
||||||
const GROWTH_QUERY = gql`
|
|
||||||
query Growth($when: String!, $from: String, $to: String)
|
|
||||||
{
|
|
||||||
registrationGrowth(when: $when, from: $from, to: $to) {
|
|
||||||
time
|
|
||||||
data {
|
|
||||||
name
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
itemGrowth(when: $when, from: $from, to: $to) {
|
|
||||||
time
|
|
||||||
data {
|
|
||||||
name
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
spendingGrowth(when: $when, from: $from, to: $to) {
|
|
||||||
time
|
|
||||||
data {
|
|
||||||
name
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
spenderGrowth(when: $when, from: $from, to: $to) {
|
|
||||||
time
|
|
||||||
data {
|
|
||||||
name
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stackingGrowth(when: $when, from: $from, to: $to) {
|
|
||||||
time
|
|
||||||
data {
|
|
||||||
name
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stackerGrowth(when: $when, from: $from, to: $to) {
|
|
||||||
time
|
|
||||||
data {
|
|
||||||
name
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ query: GROWTH_QUERY })
|
|
||||||
|
|
||||||
export default function Growth ({ ssrData }) {
|
|
||||||
const router = useRouter()
|
|
||||||
const { when, from, to } = router.query
|
|
||||||
|
|
||||||
const { data } = useQuery(GROWTH_QUERY, { variables: { when, from, to } })
|
|
||||||
if (!data && !ssrData) return <PageLoading />
|
|
||||||
|
|
||||||
const { registrationGrowth, itemGrowth, spendingGrowth, spenderGrowth, stackingGrowth, stackerGrowth } = data || ssrData
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout>
|
|
||||||
<UsageHeader />
|
|
||||||
<Row>
|
|
||||||
<Col className='mt-3'>
|
|
||||||
<div className='text-center text-muted fw-bold'>stackers</div>
|
|
||||||
<WhenLineChart data={stackerGrowth} />
|
|
||||||
</Col>
|
|
||||||
<Col className='mt-3'>
|
|
||||||
<div className='text-center text-muted fw-bold'>stacking</div>
|
|
||||||
<WhenAreaChart data={stackingGrowth} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Col className='mt-3'>
|
|
||||||
<div className='text-center text-muted fw-bold'>spenders</div>
|
|
||||||
<WhenLineChart data={spenderGrowth} />
|
|
||||||
</Col>
|
|
||||||
<Col className='mt-3'>
|
|
||||||
<div className='text-center text-muted fw-bold'>spending</div>
|
|
||||||
<WhenAreaChart data={spendingGrowth} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Col className='mt-3'>
|
|
||||||
<div className='text-center text-muted fw-bold'>registrations</div>
|
|
||||||
<WhenAreaChart data={registrationGrowth} />
|
|
||||||
</Col>
|
|
||||||
<Col className='mt-3'>
|
|
||||||
<div className='text-center text-muted fw-bold'>items</div>
|
|
||||||
<WhenComposedChart data={itemGrowth} areaNames={['posts', 'comments', 'jobs']} areaAxis='left' lineNames={['comments/posts', 'territories']} lineAxis='right' barNames={['zaps']} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Layout>
|
|
||||||
)
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user