better user analytics mostly

This commit is contained in:
keyan 2022-12-01 15:31:04 -06:00
parent 7df375e752
commit a2db3e18b4
14 changed files with 382 additions and 498 deletions

View File

@ -1,157 +1,140 @@
const PLACEHOLDERS_NUM = 616
function interval (when) {
switch (when) {
case 'week':
return '1 week'
case 'month':
return '1 month'
case 'year':
return '1 year'
case 'forever':
return null
default:
return '1 day'
}
}
function timeUnit (when) {
switch (when) {
case 'week':
case 'month':
return 'day'
case 'year':
case 'forever':
return 'month'
default:
return 'hour'
}
}
function withClause (when) {
const ival = interval(when)
const unit = timeUnit(when)
return `
WITH range_values AS (
SELECT date_trunc('${unit}', ${ival ? "now_utc() - interval '" + ival + "'" : "'2021-06-07'::timestamp"}) as minval,
date_trunc('${unit}', now_utc()) as maxval),
times AS (
SELECT generate_series(minval, maxval, interval '1 ${unit}') as time
FROM range_values
)
`
}
export default {
Query: {
registrationGrowth: async (parent, args, { models }) => {
registrationGrowth: async (parent, { when }, { models }) => {
return await models.$queryRaw(
`SELECT date_trunc('month', created_at) AS time, count("inviteId") as invited, count(*) - count("inviteId") as organic
FROM users
WHERE id > ${PLACEHOLDERS_NUM} AND date_trunc('month', now_utc()) <> date_trunc('month', created_at)
`${withClause(when)}
SELECT time, json_build_array(
json_build_object('name', 'invited', 'value', count("inviteId")),
json_build_object('name', 'organic', 'value', count(users.id) FILTER(WHERE id > ${PLACEHOLDERS_NUM}) - count("inviteId"))
) AS data
FROM times
LEFT JOIN users ON time = date_trunc('${timeUnit(when)}', created_at)
GROUP BY time
ORDER BY time ASC`)
},
activeGrowth: async (parent, args, { models }) => {
spenderGrowth: async (parent, { when }, { models }) => {
return await models.$queryRaw(
`SELECT date_trunc('month', created_at) AS time, count(DISTINCT "userId") as num
FROM "ItemAct"
WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at)
`${withClause(when)}
SELECT time, json_build_array(
json_build_object('name', 'spenders', 'value', count(DISTINCT "userId"))
) AS data
FROM times
LEFT JOIN "ItemAct" ON time = date_trunc('${timeUnit(when)}', created_at)
GROUP BY time
ORDER BY time ASC`)
},
itemGrowth: async (parent, args, { models }) => {
itemGrowth: async (parent, { when }, { models }) => {
return await models.$queryRaw(
`SELECT date_trunc('month', created_at) AS time, count("parentId") as comments,
count("subName") as jobs, count(*)-count("parentId")-count("subName") as posts
FROM "Item"
WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at)
GROUP BY time
ORDER BY time ASC`)
},
spentGrowth: async (parent, args, { models }) => {
// add up earn for each month
// add up non-self votes/tips for posts and comments
return await models.$queryRaw(
`SELECT date_trunc('month', "ItemAct".created_at) AS time,
floor(sum(CASE WHEN act = 'STREAM' THEN "ItemAct".msats ELSE 0 END)/1000) as jobs,
floor(sum(CASE WHEN act NOT IN ('BOOST', 'TIP', 'STREAM') THEN "ItemAct".msats ELSE 0 END)/1000) as fees,
floor(sum(CASE WHEN act = 'BOOST' THEN "ItemAct".msats ELSE 0 END)/1000) as boost,
floor(sum(CASE WHEN act = 'TIP' THEN "ItemAct".msats ELSE 0 END)/1000) as tips
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE date_trunc('month', now_utc()) <> date_trunc('month', "ItemAct".created_at)
GROUP BY time
ORDER BY time ASC`)
},
earnerGrowth: async (parent, args, { models }) => {
return await models.$queryRaw(
`SELECT time, count(distinct user_id) as num
FROM
((SELECT date_trunc('month', "ItemAct".created_at) AS time, "Item"."userId" as user_id
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id AND "Item"."userId" <> "ItemAct"."userId"
WHERE date_trunc('month', now_utc()) <> date_trunc('month', "ItemAct".created_at))
UNION ALL
(SELECT date_trunc('month', created_at) AS time, "userId" as user_id
FROM "Earn"
WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at))) u
GROUP BY time
ORDER BY time ASC`)
},
stackedGrowth: async (parent, args, { models }) => {
return await models.$queryRaw(
`SELECT time, floor(sum(airdrop)/1000) as rewards, floor(sum(post)/1000) as posts, floor(sum(comment)/1000) as comments
FROM
((SELECT date_trunc('month', "ItemAct".created_at) AS time, 0 as airdrop,
CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".msats END as comment,
CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id AND "Item"."userId" <> "ItemAct"."userId"
WHERE date_trunc('month', now_utc()) <> date_trunc('month', "ItemAct".created_at) AND
"ItemAct".act = 'TIP')
UNION ALL
(SELECT date_trunc('month', created_at) AS time, msats as airdrop, 0 as post, 0 as comment
FROM "Earn"
WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at))) u
GROUP BY time
ORDER BY time ASC`)
},
registrationsWeekly: async (parent, args, { models }) => {
return await models.user.count({
where: {
createdAt: {
gte: new Date(new Date().setDate(new Date().getDate() - 7))
}
}
})
},
activeWeekly: async (parent, args, { models }) => {
const [{ active }] = await models.$queryRaw(
`SELECT count(DISTINCT "userId") as active
FROM "ItemAct"
WHERE created_at >= now_utc() - interval '1 week'`
)
return active
},
earnersWeekly: async (parent, args, { models }) => {
const [{ earners }] = await models.$queryRaw(
`SELECT count(distinct user_id) as earners
FROM
((SELECT "Item"."userId" as user_id
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id AND "Item"."userId" <> "ItemAct"."userId"
WHERE "ItemAct".created_at >= now_utc() - interval '1 week')
UNION ALL
(SELECT "userId" as user_id
FROM "Earn"
WHERE created_at >= now_utc() - interval '1 week')) u`)
return earners
},
itemsWeekly: async (parent, args, { models }) => {
const [stats] = await models.$queryRaw(
`SELECT json_build_array(
`${withClause(when)}
SELECT time, json_build_array(
json_build_object('name', 'comments', 'value', count("parentId")),
json_build_object('name', 'jobs', 'value', count("subName")),
json_build_object('name', 'posts', 'value', count(*)-count("parentId")-count("subName"))) as array
FROM "Item"
WHERE created_at >= now_utc() - interval '1 week'`)
return stats?.array
json_build_object('name', 'posts', 'value', count("Item".id)-count("parentId")-count("subName"))
) AS data
FROM times
LEFT JOIN "Item" ON time = date_trunc('${timeUnit(when)}', created_at)
GROUP BY time
ORDER BY time ASC`)
},
spentWeekly: async (parent, args, { models }) => {
const [stats] = await models.$queryRaw(
`SELECT json_build_array(
json_build_object('name', 'jobs', 'value', floor(sum(CASE WHEN act = 'STREAM' THEN "ItemAct".msats ELSE 0 END)/1000)),
json_build_object('name', 'fees', 'value', floor(sum(CASE WHEN act NOT IN ('BOOST', 'TIP', 'STREAM') THEN "ItemAct".msats ELSE 0 END)/1000)),
json_build_object('name', 'boost', 'value',floor(sum(CASE WHEN act = 'BOOST' THEN "ItemAct".msats ELSE 0 END)/1000)),
json_build_object('name', 'tips', 'value', floor(sum(CASE WHEN act = 'TIP' THEN "ItemAct".msats ELSE 0 END)/1000))) as array
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE "ItemAct".created_at >= now_utc() - interval '1 week'`)
return stats?.array
spendingGrowth: async (parent, { when }, { models }) => {
return await models.$queryRaw(
`${withClause(when)}
SELECT time, json_build_array(
json_build_object('name', 'jobs', 'value', coalesce(floor(sum(CASE WHEN act = 'STREAM' THEN "ItemAct".msats ELSE 0 END)/1000),0)),
json_build_object('name', 'boost', 'value', coalesce(floor(sum(CASE WHEN act = 'BOOST' THEN "ItemAct".msats ELSE 0 END)/1000),0)),
json_build_object('name', 'fees', 'value', coalesce(floor(sum(CASE WHEN act NOT IN ('BOOST', 'TIP', 'STREAM') THEN "ItemAct".msats ELSE 0 END)/1000),0)),
json_build_object('name', 'tips', 'value', coalesce(floor(sum(CASE WHEN act = 'TIP' THEN "ItemAct".msats ELSE 0 END)/1000),0))
) AS data
FROM times
LEFT JOIN "ItemAct" ON time = date_trunc('${timeUnit(when)}', created_at)
JOIN "Item" ON "ItemAct"."itemId" = "Item".id
GROUP BY time
ORDER BY time ASC`)
},
stackedWeekly: async (parent, args, { models }) => {
const [stats] = await models.$queryRaw(
`SELECT json_build_array(
json_build_object('name', 'rewards', 'value', floor(sum(airdrop)/1000)),
json_build_object('name', 'posts', 'value', floor(sum(post)/1000)),
json_build_object('name', 'comments', 'value', floor(sum(comment)/1000))
) as array
FROM
((SELECT 0 as airdrop,
stackerGrowth: async (parent, { when }, { models }) => {
return await models.$queryRaw(
`${withClause(when)}
SELECT time, json_build_array(
json_build_object('name', 'stackers', 'value', count(distinct user_id))
) AS data
FROM times
LEFT JOIN
((SELECT "ItemAct".created_at, "Item"."userId" as user_id
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE "ItemAct".act = 'TIP')
UNION ALL
(SELECT created_at, "userId" as user_id
FROM "Earn")) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
GROUP BY time
ORDER BY time ASC`)
},
stackingGrowth: async (parent, { when }, { models }) => {
return await models.$queryRaw(
`${withClause(when)}
SELECT time, json_build_array(
json_build_object('name', 'rewards', 'value', coalesce(floor(sum(airdrop)/1000),0)),
json_build_object('name', 'posts', 'value', coalesce(floor(sum(post)/1000),0)),
json_build_object('name', 'comments', 'value', coalesce(floor(sum(comment)/1000),0))
) AS data
FROM times
LEFT JOIN
((SELECT "ItemAct".created_at, 0 as airdrop,
CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".msats END as comment,
CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id AND "Item"."userId" <> "ItemAct"."userId"
WHERE "ItemAct".created_at >= now_utc() - interval '1 week' AND
"ItemAct".act = 'TIP')
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE "ItemAct".act = 'TIP')
UNION ALL
(SELECT msats as airdrop, 0 as post, 0 as comment
FROM "Earn"
WHERE created_at >= now_utc() - interval '1 week')) u`)
return stats?.array
(SELECT created_at, msats as airdrop, 0 as post, 0 as comment
FROM "Earn")) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
GROUP BY time
ORDER BY time ASC`)
}
}
}

View File

@ -97,6 +97,7 @@ export default {
FROM "ItemAct"
JOIN users on "ItemAct"."userId" = users.id
WHERE "ItemAct".created_at <= $1
AND NOT users."hideFromTopUsers"
${within('ItemAct', when)}
GROUP BY users.id, users.name
ORDER BY spent DESC NULLS LAST, users.created_at DESC
@ -108,6 +109,7 @@ export default {
FROM users
JOIN "Item" on "Item"."userId" = users.id
WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NULL
AND NOT users."hideFromTopUsers"
${within('Item', when)}
GROUP BY users.id
ORDER BY nitems DESC NULLS LAST, users.created_at DESC
@ -119,6 +121,7 @@ export default {
FROM users
JOIN "Item" on "Item"."userId" = users.id
WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NOT NULL
AND NOT users."hideFromTopUsers"
${within('Item', when)}
GROUP BY users.id
ORDER BY ncomments DESC NULLS LAST, users.created_at DESC
@ -133,12 +136,14 @@ export default {
JOIN "Item" on "ItemAct"."itemId" = "Item".id
JOIN users on "Item"."userId" = users.id
WHERE act <> 'BOOST' AND "ItemAct"."userId" <> users.id AND "ItemAct".created_at <= $1
AND NOT users."hideFromTopUsers"
${within('ItemAct', when)})
UNION ALL
(SELECT users.*, "Earn".msats as amount
FROM "Earn"
JOIN users on users.id = "Earn"."userId"
WHERE "Earn".msats > 0 ${within('Earn', when)})) u
WHERE "Earn".msats > 0 ${within('Earn', when)}
AND NOT users."hideFromTopUsers")) u
GROUP BY u.id, u.name, u.created_at, u."photoId"
ORDER BY stacked DESC NULLS LAST, created_at DESC
OFFSET $2

View File

@ -2,24 +2,12 @@ import { gql } from 'apollo-server-micro'
export default gql`
extend type Query {
registrationGrowth: [RegistrationGrowth!]!
activeGrowth: [TimeNum!]!
itemGrowth: [ItemGrowth!]!
spentGrowth: [SpentGrowth!]!
stackedGrowth: [StackedGrowth!]!
earnerGrowth: [TimeNum!]!
registrationsWeekly: Int!
activeWeekly: Int!
earnersWeekly: Int!
itemsWeekly: [NameValue!]!
spentWeekly: [NameValue!]!
stackedWeekly: [NameValue!]!
}
type TimeNum {
time: String!
num: Int!
registrationGrowth(when: String): [TimeData!]!
itemGrowth(when: String): [TimeData!]!
spendingGrowth(when: String): [TimeData!]!
spenderGrowth(when: String): [TimeData!]!
stackingGrowth(when: String): [TimeData!]!
stackerGrowth(when: String): [TimeData!]!
}
type NameValue {
@ -27,31 +15,8 @@ export default gql`
value: Int!
}
type RegistrationGrowth {
type TimeData {
time: String!
invited: Int!
organic: Int!
}
type ItemGrowth {
time: String!
jobs: Int!
posts: Int!
comments: Int!
}
type StackedGrowth {
time: String!
rewards: Int!
posts: Int!
comments: Int!
}
type SpentGrowth {
time: String!
jobs: Int!
fees: Int!
boost: Int!
tips: Int!
data: [NameValue!]!
}
`

View File

@ -21,7 +21,7 @@ export default gql`
setName(name: String!): Boolean
setSettings(tipDefault: Int!, fiatCurrency: String!, noteItemSats: Boolean!, noteEarning: Boolean!,
noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!,
noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!,
noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!, hideFromTopUsers: Boolean!,
wildWestMode: Boolean!, greeterMode: Boolean!): User
setPhoto(photoId: ID!): Int!
upsertBio(bio: String!): User!
@ -64,6 +64,7 @@ export default gql`
noteInvites: Boolean!
noteJobIndicator: Boolean!
hideInvoiceDesc: Boolean!
hideFromTopUsers: Boolean!
wildWestMode: Boolean!
greeterMode: Boolean!
lastCheckedJobs: String

View File

@ -98,7 +98,7 @@ const AnalyticsPopover = (
visitors
</a>
<span className='mx-2 text-dark'> \ </span>
<Link href='/users/week' passHref>
<Link href='/users/day' passHref>
<a className='text-dark d-inline-flex'>
users
</a>

View File

@ -1,35 +1,26 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Nav, Navbar } from 'react-bootstrap'
import styles from './header.module.css'
import { Form, Select } from './form'
export function UsageHeader () {
const router = useRouter()
return (
<Navbar className='pt-0'>
<Nav
className={`${styles.navbarNav} justify-content-around`}
activeKey={router.asPath}
>
<Nav.Item>
<Link href='/users/week' passHref>
<Nav.Link
className={styles.navLink}
>
week
</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
<Link href='/users/forever' passHref>
<Nav.Link
className={styles.navLink}
>
forever
</Nav.Link>
</Link>
</Nav.Item>
</Nav>
</Navbar>
<Form
initial={{
when: router.query.when || 'day'
}}
>
<div className='text-muted font-weight-bold my-3 d-flex align-items-center'>
user analytics for
<Select
groupClassName='mb-0 ml-2'
className='w-auto'
name='when'
size='sm'
items={['day', 'week', 'month', 'year', 'forever']}
onChange={(formik, e) => router.push(`/users/${e.target.value}`)}
/>
</div>
</Form>
)
}

View File

@ -24,6 +24,7 @@ export const ME = gql`
noteInvites
noteJobIndicator
hideInvoiceDesc
hideFromTopUsers
wildWestMode
greeterMode
lastCheckedJobs
@ -42,6 +43,7 @@ export const SETTINGS_FIELDS = gql`
noteInvites
noteJobIndicator
hideInvoiceDesc
hideFromTopUsers
wildWestMode
greeterMode
authMethods {
@ -65,13 +67,13 @@ gql`
${SETTINGS_FIELDS}
mutation setSettings($tipDefault: Int!, $fiatCurrency: String!, $noteItemSats: Boolean!, $noteEarning: Boolean!,
$noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!,
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!,
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!, $hideFromTopUsers: Boolean!,
$wildWestMode: Boolean!, $greeterMode: Boolean!) {
setSettings(tipDefault: $tipDefault, fiatCurrency: $fiatCurrency, noteItemSats: $noteItemSats,
noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, wildWestMode: $wildWestMode,
greeterMode: $greeterMode) {
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, hideFromTopUsers: $hideFromTopUsers,
wildWestMode: $wildWestMode, greeterMode: $greeterMode) {
...SettingsFields
}
}

View File

@ -1,10 +1,7 @@
import { Nav, Navbar } from 'react-bootstrap'
import { getGetServerSideProps } from '../api/ssrApollo'
import Layout from '../components/layout'
import Notifications from '../components/notifications'
import { NOTIFICATIONS } from '../fragments/notifications'
import styles from '../components/header.module.css'
import Link from 'next/link'
import { useRouter } from 'next/router'
export const getServerSideProps = getGetServerSideProps(NOTIFICATIONS)
@ -14,7 +11,6 @@ export default function NotificationPage ({ data: { notifications: { notificatio
return (
<Layout>
<NotificationHeader />
<Notifications
notifications={notifications} cursor={cursor}
lastChecked={lastChecked} variables={{ inc: router.query?.inc }}
@ -22,34 +18,3 @@ export default function NotificationPage ({ data: { notifications: { notificatio
</Layout>
)
}
export function NotificationHeader () {
const router = useRouter()
return (
<Navbar className='py-0'>
<Nav
className={`${styles.navbarNav} justify-content-around`}
activeKey={router.asPath}
>
<Nav.Item>
<Link href='/notifications' passHref>
<Nav.Link
className={styles.navLink}
>
all
</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
<Link href='/notifications?inc=replies' passHref>
<Nav.Link
className={styles.navLink}
>
replies
</Nav.Link>
</Link>
</Nav.Item>
</Nav>
</Navbar>
)
}

View File

@ -13,6 +13,7 @@ import { SETTINGS, SET_SETTINGS } from '../fragments/users'
import { useRouter } from 'next/router'
import Info from '../components/info'
import { CURRENCY_SYMBOLS } from '../components/price'
import Link from 'next/link'
export const getServerSideProps = getGetServerSideProps(SETTINGS)
@ -67,6 +68,7 @@ export default function Settings ({ data: { settings } }) {
noteInvites: settings?.noteInvites,
noteJobIndicator: settings?.noteJobIndicator,
hideInvoiceDesc: settings?.hideInvoiceDesc,
hideFromTopUsers: settings?.hideFromTopUsers,
wildWestMode: settings?.wildWestMode,
greeterMode: settings?.greeterMode
}}
@ -144,6 +146,11 @@ export default function Settings ({ data: { settings } }) {
</div>
}
name='hideInvoiceDesc'
groupClassName='mb-0'
/>
<Checkbox
label={<>hide me from <Link href='/top/users' passHref><a>top users</a></Link></>}
name='hideFromTopUsers'
/>
<div className='form-label'>content</div>
<Checkbox

214
pages/users/[when].js Normal file
View File

@ -0,0 +1,214 @@
import { gql } from '@apollo/client'
import { getGetServerSideProps } from '../../api/ssrApollo'
import Layout from '../../components/layout'
import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, AreaChart, Area } from 'recharts'
import { Col, Row } from 'react-bootstrap'
import { abbrNum } from '../../lib/format'
import { UsageHeader } from '../../components/usage-header'
import { useRouter } from 'next/router'
export const getServerSideProps = getGetServerSideProps(
gql`
query Growth($when: String!)
{
registrationGrowth(when: $when) {
time
data {
name
value
}
}
itemGrowth(when: $when) {
time
data {
name
value
}
}
spendingGrowth(when: $when) {
time
data {
name
value
}
}
spenderGrowth(when: $when) {
time
data {
name
value
}
}
stackingGrowth(when: $when) {
time
data {
name
value
}
}
stackerGrowth(when: $when) {
time
data {
name
value
}
}
}`)
// todo: this needs to accomodate hours, days, months now
const dateFormatter = when => {
return timeStr => {
const date = new Date(timeStr)
switch (when) {
case 'week':
case 'month':
return `${('0' + (date.getMonth() % 12 + 1)).slice(-2)}/${date.getDate()}`
case 'year':
case 'forever':
return `${('0' + (date.getMonth() % 12 + 1)).slice(-2)}/${String(date.getFullYear()).slice(-2)}`
default:
return `${date.getHours() % 12 || 12}${date.getHours() >= 12 ? 'p' : 'a'}`
}
}
}
function xAxisName (when) {
switch (when) {
case 'week':
case 'month':
return 'days'
case 'year':
case 'forever':
return 'months'
default:
return 'hours'
}
}
const transformData = data => {
return data.map(entry => {
const obj = { time: entry.time }
entry.data.forEach(entry1 => {
obj[entry1.name] = entry1.value
})
return obj
})
}
export default function Growth ({
data: { registrationGrowth, itemGrowth, spendingGrowth, spenderGrowth, stackingGrowth, stackerGrowth }
}) {
return (
<Layout>
<UsageHeader />
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>stackers</div>
<GrowthLineChart data={stackerGrowth} />
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>stacking</div>
<GrowthAreaChart data={stackingGrowth} />
</Col>
</Row>
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>spenders</div>
<GrowthLineChart data={spenderGrowth} />
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>spending</div>
<GrowthAreaChart data={spendingGrowth} />
</Col>
</Row>
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>registrations</div>
<GrowthAreaChart data={registrationGrowth} />
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>items</div>
<GrowthAreaChart data={itemGrowth} />
</Col>
</Row>
</Layout>
)
}
const COLORS = [
'var(--secondary)',
'var(--info)',
'var(--success)',
'var(--boost)',
'var(--grey)'
]
function GrowthAreaChart ({ data }) {
const router = useRouter()
if (!data || data.length === 0) {
return null
}
// transform data into expected shape
data = transformData(data)
// need to grab when
const when = router.query.when
return (
<ResponsiveContainer width='100%' height={300} minWidth={300}>
<AreaChart
data={data}
margin={{
top: 5,
right: 5,
left: 0,
bottom: 0
}}
>
<XAxis
dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
tick={{ fill: 'var(--theme-grey)' }}
/>
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
<Legend />
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
<Area key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
</AreaChart>
</ResponsiveContainer>
)
}
function GrowthLineChart ({ data }) {
const router = useRouter()
if (!data || data.length === 0) {
return null
}
// transform data into expected shape
data = transformData(data)
// need to grab when
const when = router.query.when
return (
<ResponsiveContainer width='100%' height={300} minWidth={300}>
<LineChart
data={data}
margin={{
top: 5,
right: 5,
left: 0,
bottom: 0
}}
>
<XAxis
dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
tick={{ fill: 'var(--theme-grey)' }}
/>
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
<Legend />
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
<Line key={v} type='monotone' dataKey={v} name={v} stroke={COLORS[i]} fill={COLORS[i]} />)}
</LineChart>
</ResponsiveContainer>
)
}

View File

@ -1,151 +0,0 @@
import { gql } from '@apollo/client'
import { getGetServerSideProps } from '../../api/ssrApollo'
import Layout from '../../components/layout'
import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, AreaChart, Area } from 'recharts'
import { Col, Row } from 'react-bootstrap'
import { abbrNum } from '../../lib/format'
import { UsageHeader } from '../../components/usage-header'
export const getServerSideProps = getGetServerSideProps(
gql`
{
registrationGrowth {
time
invited
organic
}
activeGrowth {
time
num
}
itemGrowth {
time
jobs
comments
posts
}
spentGrowth {
time
jobs
fees
boost
tips
}
stackedGrowth {
time
posts
comments
rewards
}
earnerGrowth {
time
num
}
}`)
const dateFormatter = timeStr => {
const date = new Date(timeStr)
return `${('0' + (date.getUTCMonth() % 12 + 1)).slice(-2)}/${String(date.getUTCFullYear()).slice(-2)}`
}
export default function Growth ({
data: { registrationGrowth, activeGrowth, itemGrowth, spentGrowth, earnerGrowth, stackedGrowth }
}) {
return (
<Layout>
<UsageHeader />
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold invisible'>stackers</div>
<GrowthLineChart data={earnerGrowth} xName='month' yName='stackers' />
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>stacking</div>
<GrowthAreaChart data={stackedGrowth} xName='month' />
</Col>
</Row>
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>items</div>
<GrowthAreaChart data={itemGrowth} xName='month' />
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>spending</div>
<GrowthAreaChart data={spentGrowth} xName='month' yName='sats spent' />
</Col>
</Row>
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>registrations</div>
<GrowthAreaChart data={registrationGrowth} xName='month' yName='registrations' />
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold invisible'>spenders</div>
<GrowthLineChart data={activeGrowth} xName='month' yName='spenders' />
</Col>
</Row>
</Layout>
)
}
const COLORS = [
'var(--secondary)',
'var(--info)',
'var(--success)',
'var(--boost)',
'var(--grey)'
]
function GrowthAreaChart ({ data, xName, title }) {
if (!data || data.length === 0) {
return null
}
return (
<ResponsiveContainer width='100%' height={300} minWidth={300}>
<AreaChart
data={data}
margin={{
top: 5,
right: 5,
left: 0,
bottom: 0
}}
>
<XAxis
dataKey='time' tickFormatter={dateFormatter} name={xName}
tick={{ fill: 'var(--theme-grey)' }}
/>
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
<Tooltip labelFormatter={dateFormatter} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
<Legend />
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
<Area key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
</AreaChart>
</ResponsiveContainer>
)
}
function GrowthLineChart ({ data, xName, yName }) {
return (
<ResponsiveContainer width='100%' height={300} minWidth={300}>
<LineChart
data={data}
margin={{
top: 5,
right: 5,
left: 0,
bottom: 0
}}
>
<XAxis
dataKey='time' tickFormatter={dateFormatter} name={xName}
tick={{ fill: 'var(--theme-grey)' }}
/>
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
<Tooltip labelFormatter={dateFormatter} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
<Legend />
<Line type='monotone' dataKey='num' name={yName} stroke='var(--secondary)' />
</LineChart>
</ResponsiveContainer>
)
}

View File

@ -1,101 +0,0 @@
import { gql } from '@apollo/client'
import { getGetServerSideProps } from '../../api/ssrApollo'
import Layout from '../../components/layout'
import { Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
import { Col, Row } from 'react-bootstrap'
import { UsageHeader } from '../../components/usage-header'
export const getServerSideProps = getGetServerSideProps(
gql`
{
registrationsWeekly
activeWeekly
earnersWeekly
itemsWeekly {
name
value
}
spentWeekly {
name
value
}
stackedWeekly {
name
value
}
}`)
export default function Growth ({
data: {
registrationsWeekly, activeWeekly, itemsWeekly, spentWeekly,
stackedWeekly, earnersWeekly
}
}) {
return (
<Layout>
<UsageHeader />
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>registrations</div>
<h3 className='text-center'>{registrationsWeekly}</h3>
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>spenders</div>
<h3 className='text-center'>{activeWeekly}</h3>
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>stackers</div>
<h3 className='text-center'>{earnersWeekly}</h3>
</Col>
</Row>
<Row>
<Col className='mt-3 p-0'>
<div className='text-center text-muted font-weight-bold'>items</div>
<GrowthPieChart data={itemsWeekly} />
</Col>
<Col className='mt-3 p-0'>
<div className='text-center text-muted font-weight-bold'>spent</div>
<GrowthPieChart data={spentWeekly} />
</Col>
<Col className='mt-3 p-0'>
<div className='text-center text-muted font-weight-bold'>stacked</div>
<GrowthPieChart data={stackedWeekly} />
</Col>
</Row>
</Layout>
)
}
const COLORS = [
'var(--secondary)',
'var(--info)',
'var(--success)',
'var(--boost)',
'var(--grey)'
]
function GrowthPieChart ({ data }) {
return (
<ResponsiveContainer width='100%' height={250} minWidth={200}>
<PieChart margin={{ top: 5, right: 5, bottom: 5, left: 5 }}>
<Pie
dataKey='value'
isAnimationActive={false}
data={data}
cx='50%'
cy='50%'
outerRadius={80}
fill='var(--secondary)'
label
>
{
data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index]} />
))
}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
)
}

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "hideFromTopUsers" BOOLEAN NOT NULL DEFAULT false;

View File

@ -58,7 +58,8 @@ model User {
noteJobIndicator Boolean @default(true)
// privacy settings
hideInvoiceDesc Boolean @default(false)
hideInvoiceDesc Boolean @default(false)
hideFromTopUsers Boolean @default(false)
// content settings
wildWestMode Boolean @default(false)