diff --git a/api/resolvers/growth.js b/api/resolvers/growth.js index 3faa3cac..e0712766 100644 --- a/api/resolvers/growth.js +++ b/api/resolvers/growth.js @@ -20,21 +20,30 @@ export default { }, itemGrowth: async (parent, args, { models }) => { return await models.$queryRaw( - `SELECT date_trunc('month', created_at) AS time, count(*) as num + `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', created_at) AS time, sum(sats) as num + `SELECT date_trunc('month', "ItemAct".created_at) AS time, + sum(CASE WHEN act = 'STREAM' THEN sats ELSE 0 END) as jobs, + sum(CASE WHEN act = 'VOTE' AND "Item"."userId" = "ItemAct"."userId" THEN sats ELSE 0 END) as fees, + sum(CASE WHEN act = 'BOOST' THEN sats ELSE 0 END) as boost, + sum(CASE WHEN act = 'TIP' THEN sats ELSE 0 END) as tips FROM "ItemAct" - WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at) + 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`) }, - earnedGrowth: async (parent, args, { models }) => { + earnerGrowth: async (parent, args, { models }) => { return await models.$queryRaw( `SELECT time, count(distinct user_id) as num FROM @@ -48,6 +57,101 @@ export default { 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, sum(airdrop) as airdrops, sum(post) as posts, sum(comment) as comments + FROM + ((SELECT date_trunc('month', "ItemAct".created_at) AS time, 0 as airdrop, + CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE sats END as comment, + CASE WHEN "Item"."parentId" IS NULL THEN sats 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 IN ('VOTE', 'TIP')) + UNION ALL + (SELECT date_trunc('month', created_at) AS time, msats / 1000 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.item.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', 'job', '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 + }, + spentWeekly: async (parent, args, { models }) => { + const [stats] = await models.$queryRaw( + `SELECT json_build_array( + json_build_object('name', 'jobs', 'value', sum(CASE WHEN act = 'STREAM' THEN sats ELSE 0 END)), + json_build_object('name', 'fees', 'value', sum(CASE WHEN act = 'VOTE' AND "Item"."userId" = "ItemAct"."userId" THEN sats ELSE 0 END)), + json_build_object('name', 'boost', 'value', sum(CASE WHEN act = 'BOOST' THEN sats ELSE 0 END)), + json_build_object('name', 'tips', 'value', sum(CASE WHEN act = 'TIP' THEN sats ELSE 0 END))) as array + FROM "ItemAct" + JOIN "Item" on "ItemAct"."itemId" = "Item".id + WHERE "ItemAct".created_at >= now_utc() - interval '1 week'`) + + return stats?.array + }, + stackedWeekly: async (parent, args, { models }) => { + const [stats] = await models.$queryRaw( + `SELECT json_build_array( + json_build_object('name', 'airdrops', 'value', sum(airdrop)), + json_build_object('name', 'posts', 'value', sum(post)), + json_build_object('name', 'comments', 'value', sum(comment)) + ) as array + FROM + ((SELECT 0 as airdrop, + CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE sats END as comment, + CASE WHEN "Item"."parentId" IS NULL THEN sats 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 IN ('VOTE', 'TIP')) + UNION ALL + (SELECT msats / 1000 as airdrop, 0 as post, 0 as comment + FROM "Earn" + WHERE created_at >= now_utc() - interval '1 week')) u`) + + return stats?.array } } } diff --git a/api/typeDefs/growth.js b/api/typeDefs/growth.js index bab68095..f3467fc7 100644 --- a/api/typeDefs/growth.js +++ b/api/typeDefs/growth.js @@ -4,13 +4,48 @@ export default gql` extend type Query { registrationGrowth: [TimeNum!]! activeGrowth: [TimeNum!]! - itemGrowth: [TimeNum!]! - spentGrowth: [TimeNum!]! - earnedGrowth: [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! } + + type NameValue { + name: String! + value: Int! + } + + type ItemGrowth { + time: String! + jobs: Int! + posts: Int! + comments: Int! + } + + type StackedGrowth { + time: String! + airdrops: Int! + posts: Int! + comments: Int! + } + + type SpentGrowth { + time: String! + jobs: Int! + fees: Int! + boost: Int! + tips: Int! + } ` diff --git a/components/footer.js b/components/footer.js index 05dfc70a..10d1e675 100644 --- a/components/footer.js +++ b/components/footer.js @@ -96,9 +96,9 @@ const AnalyticsPopover = ( visitors \ - + - usage + users diff --git a/components/usage-header.js b/components/usage-header.js new file mode 100644 index 00000000..1d2c30ba --- /dev/null +++ b/components/usage-header.js @@ -0,0 +1,35 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' +import { Nav, Navbar } from 'react-bootstrap' +import styles from './header.module.css' + +export function UsageHeader () { + const router = useRouter() + return ( + + + + ) +} diff --git a/pages/usage.js b/pages/usage.js deleted file mode 100644 index 5e6f3817..00000000 --- a/pages/usage.js +++ /dev/null @@ -1,92 +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 } from 'recharts' -import { Col, Row } from 'react-bootstrap' -import { formatSats } from '../lib/format' - -export const getServerSideProps = getGetServerSideProps( - gql` - { - registrationGrowth { - time - num - } - activeGrowth { - time - num - } - itemGrowth { - time - num - } - spentGrowth { - time - num - } - earnedGrowth { - time - num - } - }`) - -const dateFormatter = timeStr => { - const date = new Date(timeStr) - return `${('0' + (date.getMonth() + 2)).slice(-2)}/${String(date.getFullYear()).slice(-2)}` -} - -export default function Growth ({ - data: { registrationGrowth, activeGrowth, itemGrowth, spentGrowth, earnedGrowth } -}) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - ) -} - -function GrowthLineChart ({ data, xName, yName }) { - return ( - - - - - - - - - - ) -} diff --git a/pages/users/forever.js b/pages/users/forever.js new file mode 100644 index 00000000..7ede0a3f --- /dev/null +++ b/pages/users/forever.js @@ -0,0 +1,147 @@ +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 { formatSats } from '../../lib/format' +import { UsageHeader } from '../../components/usage-header' + +export const getServerSideProps = getGetServerSideProps( + gql` + { + registrationGrowth { + time + num + } + activeGrowth { + time + num + } + itemGrowth { + time + jobs + comments + posts + } + spentGrowth { + time + jobs + fees + boost + tips + } + stackedGrowth { + time + posts + comments + airdrops + } + earnerGrowth { + time + num + } + }`) + +const dateFormatter = timeStr => { + const date = new Date(timeStr) + return `${('0' + (date.getMonth() + 2)).slice(-2)}/${String(date.getFullYear()).slice(-2)}` +} + +export default function Growth ({ + data: { registrationGrowth, activeGrowth, itemGrowth, spentGrowth, earnerGrowth, stackedGrowth } +}) { + return ( + + + + +
earning users
+ + + +
stacking
+ + +
+ + +
items
+ + + +
spending
+ + +
+ + +
registrations
+ + + +
active users
+ + +
+
+ ) +} + +const COLORS = [ + 'var(--secondary)', + 'var(--info)', + 'var(--success)', + 'var(--boost)', + 'var(--grey)' +] + +function GrowthAreaChart ({ data, xName, title }) { + return ( + + + + + + + {Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) => + )} + + + ) +} + +function GrowthLineChart ({ data, xName, yName }) { + return ( + + + + + + + + + + ) +} diff --git a/pages/users/week.js b/pages/users/week.js new file mode 100644 index 00000000..2f938583 --- /dev/null +++ b/pages/users/week.js @@ -0,0 +1,101 @@ +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 ( + + + + +
registrations
+

{registrationsWeekly}

+ + +
interactive users
+

{activeWeekly}

+ + +
earners
+

{earnersWeekly}

+ +
+ + +
items
+ + + +
stacked
+ + + +
spent
+ + +
+
+ ) +} + +const COLORS = [ + 'var(--secondary)', + 'var(--info)', + 'var(--success)', + 'var(--boost)', + 'var(--grey)' +] + +function GrowthPieChart ({ data }) { + return ( + + + + { + data.map((entry, index) => ( + + )) + } + + + + + ) +}