From e4831e65d5dfe57400d821cdcf6973642943e46f Mon Sep 17 00:00:00 2001 From: keyan Date: Tue, 15 Aug 2023 12:41:51 -0500 Subject: [PATCH] show sources and history of rewards --- api/resolvers/rewards.js | 55 ++++++++++++++---- api/typeDefs/rewards.js | 17 +++++- components/charts.js | 6 +- components/footer-rewards.js | 4 +- components/notifications.js | 2 +- fragments/rewards.js | 33 +++++++++++ pages/rewards/[when].js | 78 ++++++++++++++++++++++++++ pages/{rewards.js => rewards/index.js} | 24 ++++---- pages/satistics.js | 6 +- styles/globals.scss | 6 +- svgs/trophy-fill.svg | 1 + 11 files changed, 198 insertions(+), 34 deletions(-) create mode 100644 fragments/rewards.js create mode 100644 pages/rewards/[when].js rename pages/{rewards.js => rewards/index.js} (82%) create mode 100644 svgs/trophy-fill.svg diff --git a/api/resolvers/rewards.js b/api/resolvers/rewards.js index bb0d8f55..0a98c845 100644 --- a/api/resolvers/rewards.js +++ b/api/resolvers/rewards.js @@ -5,33 +5,64 @@ import { ANON_USER_ID } from '../../lib/constants' export default { Query: { - expectedRewards: async (parent, args, { models }) => { + rewards: async (parent, { when }, { models }) => { + if (when && isNaN(new Date(when))) { + throw new GraphQLError('invalid date', { extensions: { code: 'BAD_USER_INPUT' } }) + } + const [result] = await models.$queryRaw` - SELECT coalesce(FLOOR(sum(sats)), 0) as total, json_build_array( - json_build_object('name', 'donations', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'DONATION')), 0)), - json_build_object('name', 'fees', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type NOT IN ('BOOST', 'STREAM', 'DONATION', 'ANON'))), 0)), - json_build_object('name', 'boost', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'BOOST')), 0)), - json_build_object('name', 'jobs', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'STREAM')), 0)), - json_build_object('name', 'anon earnings', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'ANON')), 0)) + WITH day_cte (day) AS ( + SELECT COALESCE(${when}::text::timestamp - interval '1 day', date_trunc('day', now() AT TIME ZONE 'America/Chicago')) + ) + SELECT coalesce(FLOOR(sum(sats)), 0) as total, + COALESCE(${when}::text::timestamp, date_trunc('day', (now() + interval '1 day') AT TIME ZONE 'America/Chicago')) as time, + json_build_array( + json_build_object('name', 'donations', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'DONATION')), 0)), + json_build_object('name', 'fees', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type NOT IN ('BOOST', 'STREAM', 'DONATION', 'ANON'))), 0)), + json_build_object('name', 'boost', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'BOOST')), 0)), + json_build_object('name', 'jobs', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'STREAM')), 0)), + json_build_object('name', 'anon''s stack', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'ANON')), 0)) ) AS sources - FROM ( + FROM day_cte + CROSS JOIN LATERAL ( (SELECT ("ItemAct".msats - COALESCE("ReferralAct".msats, 0)) / 1000.0 as sats, act::text as type FROM "ItemAct" LEFT JOIN "ReferralAct" ON "ReferralAct"."itemActId" = "ItemAct".id - WHERE date_trunc('day', "ItemAct".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = date_trunc('day', now() AT TIME ZONE 'America/Chicago') AND "ItemAct".act <> 'TIP') + WHERE date_trunc('day', "ItemAct".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = day_cte.day AND "ItemAct".act <> 'TIP') UNION ALL (SELECT sats::FLOAT, 'DONATION' as type FROM "Donation" - WHERE date_trunc('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = date_trunc('day', now() AT TIME ZONE 'America/Chicago')) + WHERE date_trunc('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = day_cte.day) UNION ALL (SELECT "ItemAct".msats / 1000.0 as sats, 'ANON' as type FROM "Item" JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id WHERE "Item"."userId" = ${ANON_USER_ID} AND "ItemAct".act = 'TIP' AND "Item"."fwdUserId" IS NULL - AND date_trunc('day', "ItemAct".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = date_trunc('day', now() AT TIME ZONE 'America/Chicago')) + AND date_trunc('day', "ItemAct".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = day_cte.day) ) subquery` - return result || { total: 0, sources: [] } + return result || { total: 0, time: 0, sources: [] } + }, + meRewards: async (parent, { when }, { me, models }) => { + if (!me) { + return null + } + + const [result] = await models.$queryRaw` + WITH day_cte (day) AS ( + SELECT date_trunc('day', ${when}::text::timestamp AT TIME ZONE 'America/Chicago') + ) + SELECT coalesce(sum(sats), 0) as total, json_agg("Earn".*) as rewards + FROM day_cte + CROSS JOIN LATERAL ( + (SELECT FLOOR("Earn".msats / 1000.0) as sats, type, rank + FROM "Earn" + WHERE "Earn"."userId" = ${me.id} + AND date_trunc('day', "Earn".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = day_cte.day + ORDER BY "Earn".msats DESC) + ) "Earn"` + + return result } }, Mutation: { diff --git a/api/typeDefs/rewards.js b/api/typeDefs/rewards.js index 85ca1ba4..e00c9661 100644 --- a/api/typeDefs/rewards.js +++ b/api/typeDefs/rewards.js @@ -2,15 +2,28 @@ import { gql } from 'graphql-tag' export default gql` extend type Query { - expectedRewards: ExpectedRewards! + rewards(when: String): Rewards! + meRewards(when: String!): MeRewards } extend type Mutation { donateToRewards(sats: Int!): Int! } - type ExpectedRewards { + type Rewards { total: Int! + time: Date! sources: [NameValue!]! } + + type Reward { + type: String + rank: Int + sats: Int! + } + + type MeRewards { + total: Int! + rewards: [Reward!] + } ` diff --git a/components/charts.js b/components/charts.js index 7c5a2c27..1b12fc3b 100644 --- a/components/charts.js +++ b/components/charts.js @@ -179,15 +179,19 @@ export function WhenComposedChart ({ } export function GrowthPieChart ({ data }) { + const nonZeroData = data.filter(d => d.value > 0) + return ( diff --git a/components/notifications.js b/components/notifications.js index 103c87ce..03ca02a1 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -73,7 +73,7 @@ function NotificationLayout ({ children, nid, href, as, fresh }) { const defaultOnClick = n => { const type = n.__typename - if (type === 'Earn') return {} + if (type === 'Earn') return { href: `/rewards/${new Date(n.sortTime).toISOString().slice(0, 10)}` } if (type === 'Invitification') return { href: '/invites' } if (type === 'InvoicePaid') return { href: `/invoices/${n.invoice.id}` } if (type === 'Referral') return { href: '/referrals/month' } diff --git a/fragments/rewards.js b/fragments/rewards.js new file mode 100644 index 00000000..191b5569 --- /dev/null +++ b/fragments/rewards.js @@ -0,0 +1,33 @@ +import gql from 'graphql-tag' + +export const REWARDS = gql` + query rewards($when: String) { + rewards(when: $when) { + total + time + sources { + name + value + } + } + }` + +export const ME_REWARDS = gql` + query meRewards($when: String) { + rewards(when: $when) { + total + time + sources { + name + value + } + } + meRewards(when: $when) { + total + rewards { + type + rank + sats + } + } + }` diff --git a/pages/rewards/[when].js b/pages/rewards/[when].js new file mode 100644 index 00000000..2e234deb --- /dev/null +++ b/pages/rewards/[when].js @@ -0,0 +1,78 @@ +import { useQuery } from '@apollo/client' +import PageLoading from '../../components/page-loading' +import { ME_REWARDS } from '../../fragments/rewards' +import { CenterLayout } from '../../components/layout' +import dynamic from 'next/dynamic' +import { useRouter } from 'next/router' +import { getGetServerSideProps } from '../../api/ssrApollo' +import { fixedDecimal } from '../../lib/format' +import Trophy from '../../svgs/trophy-fill.svg' + +const GrowthPieChart = dynamic(() => import('../../components/charts').then(mod => mod.GrowthPieChart), { + loading: () =>
Loading...
+}) + +export const getServerSideProps = getGetServerSideProps(ME_REWARDS, null, + (data, params) => data.rewards.total === 0 || new Date(data.rewards.time) > new Date()) + +const timeString = when => new Date(when).toISOString().slice(0, 10) + +export default function Rewards ({ ssrData }) { + const router = useRouter() + const { data } = useQuery(ME_REWARDS, { variables: { ...router.query } }) + if (!data && !ssrData) return + + const { rewards: { total, sources, time }, meRewards } = data || ssrData + const when = router.query.when + + return ( + +
+

+ {when &&
On {timeString(time)} at 12a CT
} + {total} sats were rewarded +

+
+ +
+ {meRewards && + <> +

+ you earned {meRewards.total} sats ({fixedDecimal(meRewards.total * 100 / total, 2)}%) +

+
+ {meRewards.rewards?.map((r, i) => )} +
+ } +
+
+ ) +} + +function Reward ({ rank, type, sats }) { + if (!rank) return null + + const color = rank <= 3 ? 'text-primary' : 'text-muted' + + let category = type + switch (type) { + case 'TIP_POST': + category = 'in post zapping' + break + case 'TIP_COMMENT': + category = 'in comment zapping' + break + case 'POST': + category = 'among posts' + break + case 'COMMENT': + category = 'among comments' + break + } + + return ( +
+ #{rank} {category} for {sats} sats +
+ ) +} diff --git a/pages/rewards.js b/pages/rewards/index.js similarity index 82% rename from pages/rewards.js rename to pages/rewards/index.js index 4a20cb96..49328847 100644 --- a/pages/rewards.js +++ b/pages/rewards/index.js @@ -2,26 +2,26 @@ import { gql } from 'graphql-tag' import { useMemo } from 'react' import Button from 'react-bootstrap/Button' import InputGroup from 'react-bootstrap/InputGroup' -import { getGetServerSideProps } from '../api/ssrApollo' -import { Form, Input, SubmitButton } from '../components/form' -import { CenterLayout } from '../components/layout' +import { getGetServerSideProps } from '../../api/ssrApollo' +import { Form, Input, SubmitButton } from '../../components/form' +import { CenterLayout } from '../../components/layout' import { useMutation, useQuery } from '@apollo/client' import Link from 'next/link' -import { amountSchema } from '../lib/validate' +import { amountSchema } from '../../lib/validate' import Countdown from 'react-countdown' -import { numWithUnits } from '../lib/format' -import PageLoading from '../components/page-loading' -import { useShowModal } from '../components/modal' +import { numWithUnits } from '../../lib/format' +import PageLoading from '../../components/page-loading' +import { useShowModal } from '../../components/modal' import dynamic from 'next/dynamic' -import { SSR } from '../lib/constants' +import { SSR } from '../../lib/constants' -const GrowthPieChart = dynamic(() => import('../components/charts').then(mod => mod.GrowthPieChart), { +const GrowthPieChart = dynamic(() => import('../../components/charts').then(mod => mod.GrowthPieChart), { loading: () =>
Loading...
}) const REWARDS = gql` { - expectedRewards { + rewards { total sources { name @@ -66,7 +66,7 @@ export default function Rewards ({ ssrData }) { const { data } = useQuery(REWARDS, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' }) if (!data && !ssrData) return - const { expectedRewards: { total, sources } } = data || ssrData + const { rewards: { total, sources } } = data || ssrData return ( @@ -74,7 +74,7 @@ export default function Rewards ({ ssrData }) {
- + learn about rewards diff --git a/pages/satistics.js b/pages/satistics.js index d874e4f5..66846656 100644 --- a/pages/satistics.js +++ b/pages/satistics.js @@ -87,9 +87,9 @@ function Satus ({ status }) { function Detail ({ fact }) { if (fact.type === 'earn') { return ( -
- SN distributes the sats it earns back to its best stackers daily. These sats come from jobs, boosts, posting fees, and donations. You can see the daily rewards pool and make a donation here. -
+ + SN distributes the sats it earns back to its best stackers daily. These sats come from jobs, boosts, posting fees, and donations. + ) } if (fact.type === 'donation') { diff --git a/styles/globals.scss b/styles/globals.scss index fde649e6..c9909684 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -22,7 +22,7 @@ $theme-colors: ( "grey" : #e9ecef, "grey-medium" : #d2d2d2, "grey-darkmode": #8c8c8c, - "nostr": #8d45dd, + "nostr": #8d45dd ); $body-bg: #fcfcff; @@ -167,6 +167,10 @@ $grid-gutter-width: 2rem; } } +.text-primary svg { + fill: var(--bs-primary); +} + .line-height-1 { line-height: 1; } diff --git a/svgs/trophy-fill.svg b/svgs/trophy-fill.svg new file mode 100644 index 00000000..b25fe20f --- /dev/null +++ b/svgs/trophy-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file