diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index 278a89cc..236d1693 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -7,7 +7,7 @@ import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '
import { getItem, updateItem, filterClause, createItem, whereClause, muteClause } from './item'
import { ANON_USER_ID, DELETE_USER_ID, RESERVED_MAX_USER_ID, SN_NO_REWARDS_IDS } from '@/lib/constants'
import { viewGroup } from './growth'
-import { whenRange } from '@/lib/time'
+import { timeUnitForRange, whenRange } from '@/lib/time'
import assertApiKeyNotPermitted from './apiKey'
const contributors = new Set()
@@ -488,6 +488,50 @@ export default {
FROM users
WHERE (id > ${RESERVED_MAX_USER_ID} OR id IN (${ANON_USER_ID}, ${DELETE_USER_ID}))
AND SIMILARITY(name, ${q}) > ${Number(similarity) || 0.1} ORDER BY SIMILARITY(name, ${q}) DESC LIMIT ${Number(limit) || 5}`
+ },
+ userStatsActions: async (parent, { when, from, to }, { me, models }) => {
+ const range = whenRange(when, from, to)
+ return await models.$queryRawUnsafe(`
+ SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time,
+ json_build_array(
+ json_build_object('name', 'comments', 'value', COALESCE(SUM(comments), 0)),
+ json_build_object('name', 'posts', 'value', COALESCE(SUM(posts), 0)),
+ json_build_object('name', 'territories', 'value', COALESCE(SUM(territories), 0)),
+ json_build_object('name', 'referrals', 'value', COALESCE(SUM(referrals), 0))
+ ) AS data
+ FROM ${viewGroup(range, 'user_stats')}
+ WHERE id = ${me.id}
+ GROUP BY time
+ ORDER BY time ASC`, ...range)
+ },
+ userStatsIncomingSats: async (parent, { when, from, to }, { me, models }) => {
+ const range = whenRange(when, from, to)
+ return await models.$queryRawUnsafe(`
+ SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time,
+ json_build_array(
+ json_build_object('name', 'zaps', 'value', ROUND(COALESCE(SUM(msats_tipped), 0) / 1000)),
+ json_build_object('name', 'rewards', 'value', ROUND(COALESCE(SUM(msats_rewards), 0) / 1000)),
+ json_build_object('name', 'referrals', 'value', ROUND( COALESCE(SUM(msats_referrals), 0) / 1000)),
+ json_build_object('name', 'territories', 'value', ROUND(COALESCE(SUM(msats_revenue), 0) / 1000))
+ ) AS data
+ FROM ${viewGroup(range, 'user_stats')}
+ WHERE id = ${me.id}
+ GROUP BY time
+ ORDER BY time ASC`, ...range)
+ },
+ userStatsOutgoingSats: async (parent, { when, from, to }, { me, models }) => {
+ const range = whenRange(when, from, to)
+ return await models.$queryRawUnsafe(`
+ SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time,
+ json_build_array(
+ json_build_object('name', 'fees', 'value', FLOOR(COALESCE(SUM(msats_fees), 0) / 1000)),
+ json_build_object('name', 'donations', 'value', FLOOR(COALESCE(SUM(msats_donated), 0) / 1000)),
+ json_build_object('name', 'territories', 'value', FLOOR(COALESCE(SUM(msats_billing), 0) / 1000))
+ ) AS data
+ FROM ${viewGroup(range, 'user_stats')}
+ WHERE id = ${me.id}
+ GROUP BY time
+ ORDER BY time ASC`, ...range)
}
},
diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index 3113fbed..1596c371 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -14,6 +14,9 @@ export default gql`
hasNewNotes: Boolean!
mySubscribedUsers(cursor: String): Users!
myMutedUsers(cursor: String): Users!
+ userStatsActions(when: String, from: String, to: String): [TimeData!]!
+ userStatsIncomingSats(when: String, from: String, to: String): [TimeData!]!
+ userStatsOutgoingSats(when: String, from: String, to: String): [TimeData!]!
}
type UsersNullable {
@@ -182,4 +185,14 @@ export default gql`
twitterId: String
nostrAuthPubkey: String
}
+
+ type NameValue {
+ name: String!
+ value: Float!
+ }
+
+ type TimeData {
+ time: Date!
+ data: [NameValue!]!
+ }
`
diff --git a/components/nav/common.js b/components/nav/common.js
index e5114a65..2e3c7aa2 100644
--- a/components/nav/common.js
+++ b/components/nav/common.js
@@ -186,7 +186,7 @@ export function MeDropdown ({ me, dropNavKey }) {
wallet
-
+
satistics
diff --git a/components/nav/mobile/offcanvas.js b/components/nav/mobile/offcanvas.js
index ff32d177..4a295057 100644
--- a/components/nav/mobile/offcanvas.js
+++ b/components/nav/mobile/offcanvas.js
@@ -64,7 +64,7 @@ export default function OffCanvas ({ me, dropNavKey }) {
wallet
-
+
satistics
diff --git a/components/usage-header.js b/components/usage-header.js
index 925ded9c..6e76fcef 100644
--- a/components/usage-header.js
+++ b/components/usage-header.js
@@ -3,9 +3,11 @@ import { Select, DatePicker } from './form'
import { WHENS } from '@/lib/constants'
import { whenToFrom } from '@/lib/time'
-export function UsageHeader () {
+export function UsageHeader ({ pathname = null }) {
const router = useRouter()
+ const path = pathname || 'stackers'
+
const select = async values => {
const { when, ...query } = values
@@ -13,7 +15,8 @@ export function UsageHeader () {
if (query.from && !query.to) return
await router.push({
- pathname: `/stackers/${when}`,
+
+ pathname: `/${path}/${when}`,
query
})
}
diff --git a/fragments/users.js b/fragments/users.js
index 23aa658f..e6bfd085 100644
--- a/fragments/users.js
+++ b/fragments/users.js
@@ -353,3 +353,28 @@ export const USER_WITH_SUBS = gql`
}
}
}`
+
+export const USER_STATS = gql`
+ query UserStats($when: String, $from: String, $to: String) {
+ userStatsActions(when: $when, from: $from, to: $to) {
+ time
+ data {
+ name
+ value
+ }
+ }
+ userStatsIncomingSats(when: $when, from: $from, to: $to) {
+ time
+ data {
+ name
+ value
+ }
+ }
+ userStatsOutgoingSats(when: $when, from: $from, to: $to) {
+ time
+ data {
+ name
+ value
+ }
+ }
+ }`
diff --git a/pages/satistics/graphs/[when].js b/pages/satistics/graphs/[when].js
new file mode 100644
index 00000000..72aa6fc3
--- /dev/null
+++ b/pages/satistics/graphs/[when].js
@@ -0,0 +1,122 @@
+import { useQuery } from '@apollo/client'
+import { getGetServerSideProps } from '@/api/ssrApollo'
+import Layout from '@/components/layout'
+import { USER_STATS } from '@/fragments/users'
+import { useRouter } from 'next/router'
+import PageLoading from '@/components/page-loading'
+import dynamic from 'next/dynamic'
+import { numWithUnits } from '@/lib/format'
+import { UsageHeader } from '@/components/usage-header'
+import { SatisticsHeader } from '../history'
+import { WhenComposedChartSkeleton, WhenAreaChartSkeleton } from '@/components/charts-skeletons'
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
+import Tooltip from 'react-bootstrap/Tooltip'
+
+export const getServerSideProps = getGetServerSideProps({ query: USER_STATS, authRequired: true })
+
+const WhenAreaChart = dynamic(() => import('@/components/charts').then(mod => mod.WhenAreaChart), {
+ loading: () =>
+})
+const WhenComposedChart = dynamic(() => import('@/components/charts').then(mod => mod.WhenComposedChart), {
+ loading: () =>
+})
+
+const SatisticsTooltip = ({ children, overlayText }) => {
+ return (
+
+ {overlayText}
+
+ }
+ >
+
+ {children}
+
+
+ )
+}
+
+export default function Satistics ({ ssrData }) {
+ const router = useRouter()
+ const { when, from, to } = router.query
+
+ const { data } = useQuery(USER_STATS, { variables: { when, from, to } })
+ if (!data && !ssrData) return
+ const { userStatsActions, userStatsIncomingSats, userStatsOutgoingSats } = data || ssrData
+
+ const totalStacked = userStatsIncomingSats.reduce((total, a) => total + a.data?.reduce((acc, d) => acc + d.value, 0), 0)
+ const totalSpent = userStatsOutgoingSats.reduce((total, a) => total + a.data?.reduce((acc, d) => acc + d.value, 0), 0)
+ const totalEngagement = userStatsActions.reduce((total, a) => total + a.data?.reduce((acc, d) => acc + d.value, 0), 0)
+
+ return (
+
+
+
+
+
+
+
+
+
+
stacked
+
+
+
+
+ {numWithUnits(totalStacked, { abbreviate: true, format: true })}
+
+
+
+
+
+
+
spent
+
+
+
+
+ {numWithUnits(totalSpent, { abbreviate: true, format: true })}
+
+
+
+
+
+
+
actions
+
+
+
+ {new Intl.NumberFormat().format(totalEngagement)}
+
+
+
+
+
+
+ {userStatsIncomingSats.length > 0 &&
+
}
+ {userStatsOutgoingSats.length > 0 &&
+
}
+
+
+ {userStatsActions.length > 0 &&
+
}
+
+
+
+
+
+
+ )
+}
diff --git a/pages/satistics.js b/pages/satistics/history.js
similarity index 88%
rename from pages/satistics.js
rename to pages/satistics/history.js
index 39b603d6..e356dac9 100644
--- a/pages/satistics.js
+++ b/pages/satistics/history.js
@@ -1,6 +1,7 @@
import { useQuery } from '@apollo/client'
import Link from 'next/link'
import { getGetServerSideProps } from '@/api/ssrApollo'
+import Nav from 'react-bootstrap/Nav'
import Layout from '@/components/layout'
import MoreFooter from '@/components/more-footer'
import { WALLET_HISTORY } from '@/fragments/wallet'
@@ -16,6 +17,7 @@ import ItemJob from '@/components/item-job'
import PageLoading from '@/components/page-loading'
import PayerData from '@/components/payer-data'
import { Badge } from 'react-bootstrap'
+import navStyles from '../settings/settings.module.css'
export const getServerSideProps = getGetServerSideProps({ query: WALLET_HISTORY, authRequired: true })
@@ -163,6 +165,32 @@ function Fact ({ fact }) {
)
}
+export function SatisticsHeader () {
+ const router = useRouter()
+ const pathParts = router.asPath.split('?')[0].split('/').filter(segment => !!segment)
+ const activeKey = pathParts[1] ?? 'history'
+ return (
+ <>
+
satistics
+
+ >
+ )
+}
+
export default function Satistics ({ ssrData }) {
const router = useRouter()
const { data, fetchMore } = useQuery(WALLET_HISTORY, { variables: { inc: router.query.inc } })
@@ -179,7 +207,7 @@ export default function Satistics ({ ssrData }) {
}
const incstr = [...inc].join(',')
- router.push(`/satistics?inc=${incstr}`)
+ router.push(`/satistics/history?inc=${incstr}`)
}
function included (filter) {
@@ -192,7 +220,7 @@ export default function Satistics ({ ssrData }) {
return (
-
satistics
+