better user analytics mostly
This commit is contained in:
parent
7df375e752
commit
a2db3e18b4
|
@ -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`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!]!
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "hideFromTopUsers" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue