add usage mvp usage charts

This commit is contained in:
keyan 2022-06-10 14:20:33 -05:00
parent ddcb501267
commit f024cd39a2
8 changed files with 360 additions and 8 deletions

38
api/resolvers/growth.js Normal file
View File

@ -0,0 +1,38 @@
const PLACEHOLDERS_NUM = 616
export default {
Query: {
registrationGrowth: async (parent, args, { models }) => {
return await models.$queryRaw(
`SELECT date_trunc('month', created_at) AS time, count(*) as num
FROM users
WHERE id > ${PLACEHOLDERS_NUM} AND date_trunc('month', now_utc()) <> date_trunc('month', created_at)
GROUP BY time
ORDER BY time ASC`)
},
activeGrowth: async (parent, args, { 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)
GROUP BY time
ORDER BY time ASC`)
},
itemGrowth: async (parent, args, { models }) => {
return await models.$queryRaw(
`SELECT date_trunc('month', created_at) AS time, count(*) as num
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 }) => {
return await models.$queryRaw(
`SELECT date_trunc('month', created_at) AS time, sum(sats) as num
FROM "ItemAct"
WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at)
GROUP BY time
ORDER BY time ASC`)
}
}
}

View File

@ -7,7 +7,8 @@ import notifications from './notifications'
import invite from './invite'
import sub from './sub'
import upload from './upload'
import growth from './growth'
import { GraphQLJSONObject } from 'graphql-type-json'
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
upload, { JSONObject: GraphQLJSONObject }]
upload, growth, { JSONObject: GraphQLJSONObject }]

15
api/typeDefs/growth.js Normal file
View File

@ -0,0 +1,15 @@
import { gql } from 'apollo-server-micro'
export default gql`
extend type Query {
registrationGrowth: [TimeNum!]!
activeGrowth: [TimeNum!]!
itemGrowth: [TimeNum!]!
spentGrowth: [TimeNum!]!
}
type TimeNum {
time: String!
num: Int!
}
`

View File

@ -9,6 +9,7 @@ import notifications from './notifications'
import invite from './invite'
import sub from './sub'
import upload from './upload'
import growth from './growth'
const link = gql`
type Query {
@ -25,4 +26,4 @@ const link = gql`
`
export default [link, user, item, message, wallet, lnurl, notifications, invite,
sub, upload]
sub, upload, growth]

View File

@ -86,6 +86,25 @@ const ChatPopover = (
</Popover>
)
const AnalyticsPopover = (
<Popover>
<Popover.Content style={{ fontWeight: 500, fontSize: '.9rem' }}>
<a
href='https://plausible.io/stacker.news' className='text-dark d-inline-flex'
target='_blank' rel='noreferrer'
>
visitors
</a>
<span className='mx-2 text-dark'> \ </span>
<Link href='/usage' passHref>
<a className='text-dark d-inline-flex'>
usage
</a>
</Link>
</Popover.Content>
</Popover>
)
export default function Footer ({ noLinks }) {
const query = gql`
{
@ -129,12 +148,11 @@ export default function Footer ({ noLinks }) {
</a>
</Link>
<span className='mx-2 text-muted'> \ </span>
<a
href='https://plausible.io/stacker.news' className='nav-link p-0 d-inline-flex'
target='_blank' rel='noreferrer'
>
analytics
</a>
<OverlayTrigger trigger='click' placement='top' overlay={AnalyticsPopover} rootClose>
<div className='nav-link p-0 d-inline-flex' style={{ cursor: 'pointer' }}>
analytics
</div>
</OverlayTrigger>
<span className='mx-2 text-muted'> \ </span>
<OverlayTrigger trigger='click' placement='top' overlay={ChatPopover} rootClose>
<div className='nav-link p-0 d-inline-flex' style={{ cursor: 'pointer' }}>

198
package-lock.json generated
View File

@ -990,6 +990,11 @@
"form-data": "^2.5.0"
}
},
"@types/resize-observer-browser": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz",
"integrity": "sha512-G9eN0Sn0ii9PWQ3Vl72jDPgeJwRWhv2Qk/nQkJuWmRmOB4HX3/BhD5SE1dZs/hzPZL/WKnvF0RHdTSG54QJFyg=="
},
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
@ -2656,6 +2661,11 @@
}
}
},
"css-unit-converter": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz",
"integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA=="
},
"css-what": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz",
@ -2708,6 +2718,73 @@
"array-find-index": "^1.0.1"
}
},
"d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
"requires": {
"internmap": "^1.0.0"
}
},
"d3-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
"integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="
},
"d3-format": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz",
"integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA=="
},
"d3-interpolate": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
"integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
"requires": {
"d3-color": "1 - 2"
}
},
"d3-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz",
"integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA=="
},
"d3-scale": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",
"integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==",
"requires": {
"d3-array": "^2.3.0",
"d3-format": "1 - 2",
"d3-interpolate": "1.2.0 - 2",
"d3-time": "^2.1.1",
"d3-time-format": "2 - 3"
}
},
"d3-shape": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz",
"integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==",
"requires": {
"d3-path": "1 - 2"
}
},
"d3-time": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz",
"integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==",
"requires": {
"d3-array": "2"
}
},
"d3-time-format": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz",
"integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==",
"requires": {
"d3-time": "1 - 2"
}
},
"data-uri-to-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
@ -2731,6 +2808,11 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
},
"decode-named-character-reference": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.1.tgz",
@ -3531,6 +3613,11 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fast-equals": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.4.tgz",
"integrity": "sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w=="
},
"fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@ -4244,6 +4331,11 @@
"side-channel": "^1.0.4"
}
},
"internmap": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -6711,6 +6803,11 @@
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
},
"pg": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz",
@ -7048,6 +7145,11 @@
}
}
},
"postcss-value-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
},
"postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
@ -7318,6 +7420,14 @@
"inherits": "~2.0.3"
}
},
"raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"requires": {
"performance-now": "^2.1.0"
}
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -7526,6 +7636,47 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
"integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg=="
},
"react-resize-detector": {
"version": "6.7.8",
"resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.7.8.tgz",
"integrity": "sha512-0FaEcUBAbn+pq3PT5a9hHRebUfuS1SRLGLpIw8LydU7zX429I6XJgKerKAMPsJH0qWAl6o5bVKNqFJqr6tGPYw==",
"requires": {
"@types/resize-observer-browser": "^0.1.6",
"lodash": "^4.17.21",
"resize-observer-polyfill": "^1.5.1"
}
},
"react-smooth": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.0.tgz",
"integrity": "sha512-wK4dBBR6P21otowgMT9toZk+GngMplGS1O5gk+2WSiHEXIrQgDvhR5IIlT74Vtu//qpTcipkgo21dD7a7AUNxw==",
"requires": {
"fast-equals": "^2.0.0",
"raf": "^3.4.0",
"react-transition-group": "2.9.0"
},
"dependencies": {
"dom-helpers": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
"requires": {
"@babel/runtime": "^7.1.2"
}
},
"react-transition-group": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
"integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
"requires": {
"dom-helpers": "^3.4.0",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2",
"react-lifecycles-compat": "^3.0.4"
}
}
}
},
"react-string-replace": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-0.4.4.tgz",
@ -7641,6 +7792,39 @@
"picomatch": "^2.2.1"
}
},
"recharts": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.1.10.tgz",
"integrity": "sha512-me6c8m2Gs88X/nuM2gDSTDIhpSLNMbiTrlE4Cu53hjZNegT3g3xLlTrbYSAQuBCFWuWJAZXCmEuMr6AwizLyaA==",
"requires": {
"classnames": "^2.2.5",
"d3-interpolate": "^2.0.0",
"d3-scale": "^3.0.0",
"d3-shape": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.19",
"react-is": "^16.10.2",
"react-resize-detector": "^6.6.3",
"react-smooth": "^2.0.0",
"recharts-scale": "^0.4.4",
"reduce-css-calc": "^2.1.8"
},
"dependencies": {
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
}
}
},
"recharts-scale": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
"requires": {
"decimal.js-light": "^2.4.1"
}
},
"redent": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
@ -7660,6 +7844,15 @@
}
}
},
"reduce-css-calc": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz",
"integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==",
"requires": {
"css-unit-converter": "^1.1.1",
"postcss-value-parser": "^3.3.0"
}
},
"reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
@ -7784,6 +7977,11 @@
}
}
},
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"resolve": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",

View File

@ -50,6 +50,7 @@
"react-textarea-autosize": "^8.3.3",
"react-twitter-embed": "^4.0.4",
"react-youtube": "^7.14.0",
"recharts": "^2.1.10",
"remark-directive": "^2.0.1",
"remark-gfm": "^3.0.1",
"remove-markdown": "^0.3.0",

80
pages/usage.js Normal file
View File

@ -0,0 +1,80 @@
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
}
}`)
const dateFormatter = timeStr => {
const date = new Date(timeStr)
return `${('0' + (date.getMonth() + 1)).slice(-2)}/${String(date.getFullYear()).slice(-2)}`
}
export default function Growth ({ data: { registrationGrowth, activeGrowth, itemGrowth, spentGrowth } }) {
return (
<Layout>
<Row className='mt-3'>
<Col>
<GrowthLineChart data={registrationGrowth} xName='month' yName='registrations' />
</Col>
<Col>
<GrowthLineChart data={activeGrowth} xName='month' yName='active users' />
</Col>
</Row>
<Row className='mt-3'>
<Col>
<GrowthLineChart data={itemGrowth} xName='month' yName='items' />
</Col>
<Col>
<GrowthLineChart data={spentGrowth} xName='month' yName='sats spent' />
</Col>
</Row>
</Layout>
)
}
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={formatSats} 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>
)
}