better user analytics mostly
This commit is contained in:
parent
7df375e752
commit
a2db3e18b4
@ -1,157 +1,140 @@
|
|||||||
const PLACEHOLDERS_NUM = 616
|
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 {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
registrationGrowth: async (parent, args, { models }) => {
|
registrationGrowth: async (parent, { when }, { models }) => {
|
||||||
return await models.$queryRaw(
|
return await models.$queryRaw(
|
||||||
`SELECT date_trunc('month', created_at) AS time, count("inviteId") as invited, count(*) - count("inviteId") as organic
|
`${withClause(when)}
|
||||||
FROM users
|
SELECT time, json_build_array(
|
||||||
WHERE id > ${PLACEHOLDERS_NUM} AND date_trunc('month', now_utc()) <> date_trunc('month', created_at)
|
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
|
GROUP BY time
|
||||||
ORDER BY time ASC`)
|
ORDER BY time ASC`)
|
||||||
},
|
},
|
||||||
activeGrowth: async (parent, args, { models }) => {
|
spenderGrowth: async (parent, { when }, { models }) => {
|
||||||
return await models.$queryRaw(
|
return await models.$queryRaw(
|
||||||
`SELECT date_trunc('month', created_at) AS time, count(DISTINCT "userId") as num
|
`${withClause(when)}
|
||||||
FROM "ItemAct"
|
SELECT time, json_build_array(
|
||||||
WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at)
|
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
|
GROUP BY time
|
||||||
ORDER BY time ASC`)
|
ORDER BY time ASC`)
|
||||||
},
|
},
|
||||||
itemGrowth: async (parent, args, { models }) => {
|
itemGrowth: async (parent, { when }, { models }) => {
|
||||||
return await models.$queryRaw(
|
return await models.$queryRaw(
|
||||||
`SELECT date_trunc('month', created_at) AS time, count("parentId") as comments,
|
`${withClause(when)}
|
||||||
count("subName") as jobs, count(*)-count("parentId")-count("subName") as posts
|
SELECT time, json_build_array(
|
||||||
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(
|
|
||||||
json_build_object('name', 'comments', 'value', count("parentId")),
|
json_build_object('name', 'comments', 'value', count("parentId")),
|
||||||
json_build_object('name', 'jobs', 'value', count("subName")),
|
json_build_object('name', 'jobs', 'value', count("subName")),
|
||||||
json_build_object('name', 'posts', 'value', count(*)-count("parentId")-count("subName"))) as array
|
json_build_object('name', 'posts', 'value', count("Item".id)-count("parentId")-count("subName"))
|
||||||
FROM "Item"
|
) AS data
|
||||||
WHERE created_at >= now_utc() - interval '1 week'`)
|
FROM times
|
||||||
|
LEFT JOIN "Item" ON time = date_trunc('${timeUnit(when)}', created_at)
|
||||||
return stats?.array
|
GROUP BY time
|
||||||
|
ORDER BY time ASC`)
|
||||||
},
|
},
|
||||||
spentWeekly: async (parent, args, { models }) => {
|
spendingGrowth: async (parent, { when }, { models }) => {
|
||||||
const [stats] = await models.$queryRaw(
|
return await models.$queryRaw(
|
||||||
`SELECT json_build_array(
|
`${withClause(when)}
|
||||||
json_build_object('name', 'jobs', 'value', floor(sum(CASE WHEN act = 'STREAM' THEN "ItemAct".msats ELSE 0 END)/1000)),
|
SELECT time, json_build_array(
|
||||||
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', 'jobs', 'value', coalesce(floor(sum(CASE WHEN act = 'STREAM' THEN "ItemAct".msats ELSE 0 END)/1000),0)),
|
||||||
json_build_object('name', 'boost', 'value',floor(sum(CASE WHEN act = 'BOOST' THEN "ItemAct".msats ELSE 0 END)/1000)),
|
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', 'tips', 'value', floor(sum(CASE WHEN act = 'TIP' THEN "ItemAct".msats ELSE 0 END)/1000))) as array
|
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)),
|
||||||
FROM "ItemAct"
|
json_build_object('name', 'tips', 'value', coalesce(floor(sum(CASE WHEN act = 'TIP' THEN "ItemAct".msats ELSE 0 END)/1000),0))
|
||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
) AS data
|
||||||
WHERE "ItemAct".created_at >= now_utc() - interval '1 week'`)
|
FROM times
|
||||||
|
LEFT JOIN "ItemAct" ON time = date_trunc('${timeUnit(when)}', created_at)
|
||||||
return stats?.array
|
JOIN "Item" ON "ItemAct"."itemId" = "Item".id
|
||||||
|
GROUP BY time
|
||||||
|
ORDER BY time ASC`)
|
||||||
},
|
},
|
||||||
stackedWeekly: async (parent, args, { models }) => {
|
stackerGrowth: async (parent, { when }, { models }) => {
|
||||||
const [stats] = await models.$queryRaw(
|
return await models.$queryRaw(
|
||||||
`SELECT json_build_array(
|
`${withClause(when)}
|
||||||
json_build_object('name', 'rewards', 'value', floor(sum(airdrop)/1000)),
|
SELECT time, json_build_array(
|
||||||
json_build_object('name', 'posts', 'value', floor(sum(post)/1000)),
|
json_build_object('name', 'stackers', 'value', count(distinct user_id))
|
||||||
json_build_object('name', 'comments', 'value', floor(sum(comment)/1000))
|
) AS data
|
||||||
) as array
|
FROM times
|
||||||
FROM
|
LEFT JOIN
|
||||||
((SELECT 0 as airdrop,
|
((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 0 ELSE "ItemAct".msats END as comment,
|
||||||
CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post
|
CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post
|
||||||
FROM "ItemAct"
|
FROM "ItemAct"
|
||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id AND "Item"."userId" <> "ItemAct"."userId"
|
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
||||||
WHERE "ItemAct".created_at >= now_utc() - interval '1 week' AND
|
WHERE "ItemAct".act = 'TIP')
|
||||||
"ItemAct".act = 'TIP')
|
|
||||||
UNION ALL
|
UNION ALL
|
||||||
(SELECT msats as airdrop, 0 as post, 0 as comment
|
(SELECT created_at, msats as airdrop, 0 as post, 0 as comment
|
||||||
FROM "Earn"
|
FROM "Earn")) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
|
||||||
WHERE created_at >= now_utc() - interval '1 week')) u`)
|
GROUP BY time
|
||||||
|
ORDER BY time ASC`)
|
||||||
return stats?.array
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,7 @@ export default {
|
|||||||
FROM "ItemAct"
|
FROM "ItemAct"
|
||||||
JOIN users on "ItemAct"."userId" = users.id
|
JOIN users on "ItemAct"."userId" = users.id
|
||||||
WHERE "ItemAct".created_at <= $1
|
WHERE "ItemAct".created_at <= $1
|
||||||
|
AND NOT users."hideFromTopUsers"
|
||||||
${within('ItemAct', when)}
|
${within('ItemAct', when)}
|
||||||
GROUP BY users.id, users.name
|
GROUP BY users.id, users.name
|
||||||
ORDER BY spent DESC NULLS LAST, users.created_at DESC
|
ORDER BY spent DESC NULLS LAST, users.created_at DESC
|
||||||
@ -108,6 +109,7 @@ export default {
|
|||||||
FROM users
|
FROM users
|
||||||
JOIN "Item" on "Item"."userId" = users.id
|
JOIN "Item" on "Item"."userId" = users.id
|
||||||
WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NULL
|
WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NULL
|
||||||
|
AND NOT users."hideFromTopUsers"
|
||||||
${within('Item', when)}
|
${within('Item', when)}
|
||||||
GROUP BY users.id
|
GROUP BY users.id
|
||||||
ORDER BY nitems DESC NULLS LAST, users.created_at DESC
|
ORDER BY nitems DESC NULLS LAST, users.created_at DESC
|
||||||
@ -119,6 +121,7 @@ export default {
|
|||||||
FROM users
|
FROM users
|
||||||
JOIN "Item" on "Item"."userId" = users.id
|
JOIN "Item" on "Item"."userId" = users.id
|
||||||
WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NOT NULL
|
WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NOT NULL
|
||||||
|
AND NOT users."hideFromTopUsers"
|
||||||
${within('Item', when)}
|
${within('Item', when)}
|
||||||
GROUP BY users.id
|
GROUP BY users.id
|
||||||
ORDER BY ncomments DESC NULLS LAST, users.created_at DESC
|
ORDER BY ncomments DESC NULLS LAST, users.created_at DESC
|
||||||
@ -133,12 +136,14 @@ export default {
|
|||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
||||||
JOIN users on "Item"."userId" = users.id
|
JOIN users on "Item"."userId" = users.id
|
||||||
WHERE act <> 'BOOST' AND "ItemAct"."userId" <> users.id AND "ItemAct".created_at <= $1
|
WHERE act <> 'BOOST' AND "ItemAct"."userId" <> users.id AND "ItemAct".created_at <= $1
|
||||||
|
AND NOT users."hideFromTopUsers"
|
||||||
${within('ItemAct', when)})
|
${within('ItemAct', when)})
|
||||||
UNION ALL
|
UNION ALL
|
||||||
(SELECT users.*, "Earn".msats as amount
|
(SELECT users.*, "Earn".msats as amount
|
||||||
FROM "Earn"
|
FROM "Earn"
|
||||||
JOIN users on users.id = "Earn"."userId"
|
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"
|
GROUP BY u.id, u.name, u.created_at, u."photoId"
|
||||||
ORDER BY stacked DESC NULLS LAST, created_at DESC
|
ORDER BY stacked DESC NULLS LAST, created_at DESC
|
||||||
OFFSET $2
|
OFFSET $2
|
||||||
|
@ -2,24 +2,12 @@ import { gql } from 'apollo-server-micro'
|
|||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
extend type Query {
|
extend type Query {
|
||||||
registrationGrowth: [RegistrationGrowth!]!
|
registrationGrowth(when: String): [TimeData!]!
|
||||||
activeGrowth: [TimeNum!]!
|
itemGrowth(when: String): [TimeData!]!
|
||||||
itemGrowth: [ItemGrowth!]!
|
spendingGrowth(when: String): [TimeData!]!
|
||||||
spentGrowth: [SpentGrowth!]!
|
spenderGrowth(when: String): [TimeData!]!
|
||||||
stackedGrowth: [StackedGrowth!]!
|
stackingGrowth(when: String): [TimeData!]!
|
||||||
earnerGrowth: [TimeNum!]!
|
stackerGrowth(when: String): [TimeData!]!
|
||||||
|
|
||||||
registrationsWeekly: Int!
|
|
||||||
activeWeekly: Int!
|
|
||||||
earnersWeekly: Int!
|
|
||||||
itemsWeekly: [NameValue!]!
|
|
||||||
spentWeekly: [NameValue!]!
|
|
||||||
stackedWeekly: [NameValue!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type TimeNum {
|
|
||||||
time: String!
|
|
||||||
num: Int!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type NameValue {
|
type NameValue {
|
||||||
@ -27,31 +15,8 @@ export default gql`
|
|||||||
value: Int!
|
value: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegistrationGrowth {
|
type TimeData {
|
||||||
time: String!
|
time: String!
|
||||||
invited: Int!
|
data: [NameValue!]!
|
||||||
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!
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -21,7 +21,7 @@ export default gql`
|
|||||||
setName(name: String!): Boolean
|
setName(name: String!): Boolean
|
||||||
setSettings(tipDefault: Int!, fiatCurrency: String!, noteItemSats: Boolean!, noteEarning: Boolean!,
|
setSettings(tipDefault: Int!, fiatCurrency: String!, noteItemSats: Boolean!, noteEarning: Boolean!,
|
||||||
noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: 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
|
wildWestMode: Boolean!, greeterMode: Boolean!): User
|
||||||
setPhoto(photoId: ID!): Int!
|
setPhoto(photoId: ID!): Int!
|
||||||
upsertBio(bio: String!): User!
|
upsertBio(bio: String!): User!
|
||||||
@ -64,6 +64,7 @@ export default gql`
|
|||||||
noteInvites: Boolean!
|
noteInvites: Boolean!
|
||||||
noteJobIndicator: Boolean!
|
noteJobIndicator: Boolean!
|
||||||
hideInvoiceDesc: Boolean!
|
hideInvoiceDesc: Boolean!
|
||||||
|
hideFromTopUsers: Boolean!
|
||||||
wildWestMode: Boolean!
|
wildWestMode: Boolean!
|
||||||
greeterMode: Boolean!
|
greeterMode: Boolean!
|
||||||
lastCheckedJobs: String
|
lastCheckedJobs: String
|
||||||
|
@ -98,7 +98,7 @@ const AnalyticsPopover = (
|
|||||||
visitors
|
visitors
|
||||||
</a>
|
</a>
|
||||||
<span className='mx-2 text-dark'> \ </span>
|
<span className='mx-2 text-dark'> \ </span>
|
||||||
<Link href='/users/week' passHref>
|
<Link href='/users/day' passHref>
|
||||||
<a className='text-dark d-inline-flex'>
|
<a className='text-dark d-inline-flex'>
|
||||||
users
|
users
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,35 +1,26 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { Nav, Navbar } from 'react-bootstrap'
|
import { Form, Select } from './form'
|
||||||
import styles from './header.module.css'
|
|
||||||
|
|
||||||
export function UsageHeader () {
|
export function UsageHeader () {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar className='pt-0'>
|
<Form
|
||||||
<Nav
|
initial={{
|
||||||
className={`${styles.navbarNav} justify-content-around`}
|
when: router.query.when || 'day'
|
||||||
activeKey={router.asPath}
|
}}
|
||||||
>
|
>
|
||||||
<Nav.Item>
|
<div className='text-muted font-weight-bold my-3 d-flex align-items-center'>
|
||||||
<Link href='/users/week' passHref>
|
user analytics for
|
||||||
<Nav.Link
|
<Select
|
||||||
className={styles.navLink}
|
groupClassName='mb-0 ml-2'
|
||||||
>
|
className='w-auto'
|
||||||
week
|
name='when'
|
||||||
</Nav.Link>
|
size='sm'
|
||||||
</Link>
|
items={['day', 'week', 'month', 'year', 'forever']}
|
||||||
</Nav.Item>
|
onChange={(formik, e) => router.push(`/users/${e.target.value}`)}
|
||||||
<Nav.Item>
|
/>
|
||||||
<Link href='/users/forever' passHref>
|
</div>
|
||||||
<Nav.Link
|
</Form>
|
||||||
className={styles.navLink}
|
|
||||||
>
|
|
||||||
forever
|
|
||||||
</Nav.Link>
|
|
||||||
</Link>
|
|
||||||
</Nav.Item>
|
|
||||||
</Nav>
|
|
||||||
</Navbar>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ export const ME = gql`
|
|||||||
noteInvites
|
noteInvites
|
||||||
noteJobIndicator
|
noteJobIndicator
|
||||||
hideInvoiceDesc
|
hideInvoiceDesc
|
||||||
|
hideFromTopUsers
|
||||||
wildWestMode
|
wildWestMode
|
||||||
greeterMode
|
greeterMode
|
||||||
lastCheckedJobs
|
lastCheckedJobs
|
||||||
@ -42,6 +43,7 @@ export const SETTINGS_FIELDS = gql`
|
|||||||
noteInvites
|
noteInvites
|
||||||
noteJobIndicator
|
noteJobIndicator
|
||||||
hideInvoiceDesc
|
hideInvoiceDesc
|
||||||
|
hideFromTopUsers
|
||||||
wildWestMode
|
wildWestMode
|
||||||
greeterMode
|
greeterMode
|
||||||
authMethods {
|
authMethods {
|
||||||
@ -65,13 +67,13 @@ gql`
|
|||||||
${SETTINGS_FIELDS}
|
${SETTINGS_FIELDS}
|
||||||
mutation setSettings($tipDefault: Int!, $fiatCurrency: String!, $noteItemSats: Boolean!, $noteEarning: Boolean!,
|
mutation setSettings($tipDefault: Int!, $fiatCurrency: String!, $noteItemSats: Boolean!, $noteEarning: Boolean!,
|
||||||
$noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: 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!) {
|
$wildWestMode: Boolean!, $greeterMode: Boolean!) {
|
||||||
setSettings(tipDefault: $tipDefault, fiatCurrency: $fiatCurrency, noteItemSats: $noteItemSats,
|
setSettings(tipDefault: $tipDefault, fiatCurrency: $fiatCurrency, noteItemSats: $noteItemSats,
|
||||||
noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
|
noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
|
||||||
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
|
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
|
||||||
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, wildWestMode: $wildWestMode,
|
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, hideFromTopUsers: $hideFromTopUsers,
|
||||||
greeterMode: $greeterMode) {
|
wildWestMode: $wildWestMode, greeterMode: $greeterMode) {
|
||||||
...SettingsFields
|
...SettingsFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import { Nav, Navbar } from 'react-bootstrap'
|
|
||||||
import { getGetServerSideProps } from '../api/ssrApollo'
|
import { getGetServerSideProps } from '../api/ssrApollo'
|
||||||
import Layout from '../components/layout'
|
import Layout from '../components/layout'
|
||||||
import Notifications from '../components/notifications'
|
import Notifications from '../components/notifications'
|
||||||
import { NOTIFICATIONS } from '../fragments/notifications'
|
import { NOTIFICATIONS } from '../fragments/notifications'
|
||||||
import styles from '../components/header.module.css'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps(NOTIFICATIONS)
|
export const getServerSideProps = getGetServerSideProps(NOTIFICATIONS)
|
||||||
@ -14,7 +11,6 @@ export default function NotificationPage ({ data: { notifications: { notificatio
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<NotificationHeader />
|
|
||||||
<Notifications
|
<Notifications
|
||||||
notifications={notifications} cursor={cursor}
|
notifications={notifications} cursor={cursor}
|
||||||
lastChecked={lastChecked} variables={{ inc: router.query?.inc }}
|
lastChecked={lastChecked} variables={{ inc: router.query?.inc }}
|
||||||
@ -22,34 +18,3 @@ export default function NotificationPage ({ data: { notifications: { notificatio
|
|||||||
</Layout>
|
</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 { useRouter } from 'next/router'
|
||||||
import Info from '../components/info'
|
import Info from '../components/info'
|
||||||
import { CURRENCY_SYMBOLS } from '../components/price'
|
import { CURRENCY_SYMBOLS } from '../components/price'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps(SETTINGS)
|
export const getServerSideProps = getGetServerSideProps(SETTINGS)
|
||||||
|
|
||||||
@ -67,6 +68,7 @@ export default function Settings ({ data: { settings } }) {
|
|||||||
noteInvites: settings?.noteInvites,
|
noteInvites: settings?.noteInvites,
|
||||||
noteJobIndicator: settings?.noteJobIndicator,
|
noteJobIndicator: settings?.noteJobIndicator,
|
||||||
hideInvoiceDesc: settings?.hideInvoiceDesc,
|
hideInvoiceDesc: settings?.hideInvoiceDesc,
|
||||||
|
hideFromTopUsers: settings?.hideFromTopUsers,
|
||||||
wildWestMode: settings?.wildWestMode,
|
wildWestMode: settings?.wildWestMode,
|
||||||
greeterMode: settings?.greeterMode
|
greeterMode: settings?.greeterMode
|
||||||
}}
|
}}
|
||||||
@ -144,6 +146,11 @@ export default function Settings ({ data: { settings } }) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
name='hideInvoiceDesc'
|
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>
|
<div className='form-label'>content</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
214
pages/users/[when].js
Normal file
214
pages/users/[when].js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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)
|
noteJobIndicator Boolean @default(true)
|
||||||
|
|
||||||
// privacy settings
|
// privacy settings
|
||||||
hideInvoiceDesc Boolean @default(false)
|
hideInvoiceDesc Boolean @default(false)
|
||||||
|
hideFromTopUsers Boolean @default(false)
|
||||||
|
|
||||||
// content settings
|
// content settings
|
||||||
wildWestMode Boolean @default(false)
|
wildWestMode Boolean @default(false)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user