show sources and history of rewards
This commit is contained in:
parent
679f766f07
commit
e4831e65d5
|
@ -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: {
|
||||
|
|
|
@ -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!]
|
||||
}
|
||||
`
|
||||
|
|
|
@ -179,15 +179,19 @@ export function WhenComposedChart ({
|
|||
}
|
||||
|
||||
export function GrowthPieChart ({ data }) {
|
||||
const nonZeroData = data.filter(d => d.value > 0)
|
||||
|
||||
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}
|
||||
data={nonZeroData}
|
||||
cx='50%'
|
||||
cy='50%'
|
||||
minAngle={5}
|
||||
paddingAngle={0}
|
||||
outerRadius={80}
|
||||
fill='var(--bs-secondary)'
|
||||
label
|
||||
|
|
|
@ -5,14 +5,14 @@ import { SSR } from '../lib/constants'
|
|||
|
||||
const REWARDS = gql`
|
||||
{
|
||||
expectedRewards {
|
||||
rewards {
|
||||
total
|
||||
}
|
||||
}`
|
||||
|
||||
export default function Rewards () {
|
||||
const { data } = useQuery(REWARDS, SSR ? { ssr: false } : { pollInterval: 60000, nextFetchPolicy: 'cache-and-network' })
|
||||
const total = data?.expectedRewards?.total
|
||||
const total = data?.rewards?.total
|
||||
|
||||
return (
|
||||
<Link href='/rewards' className='nav-link p-0 p-0 d-inline-flex'>
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}`
|
|
@ -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: () => <div>Loading...</div>
|
||||
})
|
||||
|
||||
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 <PageLoading />
|
||||
|
||||
const { rewards: { total, sources, time }, meRewards } = data || ssrData
|
||||
const when = router.query.when
|
||||
|
||||
return (
|
||||
<CenterLayout footerLinks>
|
||||
<div className='py-3'>
|
||||
<h4 className='fw-bold text-muted ps-0'>
|
||||
{when && <div className='text-muted fst-italic fs-6 fw-normal pb-1'>On {timeString(time)} at 12a CT</div>}
|
||||
{total} sats were rewarded
|
||||
</h4>
|
||||
<div className='my-3 w-100'>
|
||||
<GrowthPieChart data={sources} />
|
||||
</div>
|
||||
{meRewards &&
|
||||
<>
|
||||
<h4 className='fw-bold text-muted text-center'>
|
||||
you earned {meRewards.total} sats ({fixedDecimal(meRewards.total * 100 / total, 2)}%)
|
||||
</h4>
|
||||
<div>
|
||||
{meRewards.rewards?.map((r, i) => <Reward key={[r.rank, r.type].join('-')} {...r} />)}
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
</CenterLayout>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={color}>
|
||||
<Trophy height={20} width={20} /> <b>#{rank}</b> {category} for <i><b>{sats} sats</b></i>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -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: () => <div>Loading...</div>
|
||||
})
|
||||
|
||||
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 <PageLoading />
|
||||
|
||||
const { expectedRewards: { total, sources } } = data || ssrData
|
||||
const { rewards: { total, sources } } = data || ssrData
|
||||
|
||||
return (
|
||||
<CenterLayout footerLinks>
|
||||
|
@ -74,7 +74,7 @@ export default function Rewards ({ ssrData }) {
|
|||
<div>
|
||||
<RewardLine total={total} />
|
||||
</div>
|
||||
<Link href='/faq#how-do-i-earn-sats-on-stacker-news' className='text-reset'>
|
||||
<Link href='/faq#how-do-i-earn-sats-on-stacker-news' className='text-info fw-normal'>
|
||||
<small><small><small>learn about rewards</small></small></small>
|
||||
</Link>
|
||||
</h4>
|
|
@ -87,9 +87,9 @@ function Satus ({ status }) {
|
|||
function Detail ({ fact }) {
|
||||
if (fact.type === 'earn') {
|
||||
return (
|
||||
<div className='px-3' style={{ lineHeight: '140%' }}>
|
||||
SN distributes the sats it earns back to its best stackers daily. These sats come from <Link href='/~jobs'>jobs</Link>, boosts, posting fees, and donations. You can see the daily rewards pool and make a donation <Link href='/rewards'>here</Link>.
|
||||
</div>
|
||||
<Link href={`/rewards/${new Date(fact.createdAt).toISOString().slice(0, 10)}`} className='px-3 text-reset' style={{ lineHeight: '140%' }}>
|
||||
SN distributes the sats it earns back to its best stackers daily. These sats come from <Link href='/~jobs'>jobs</Link>, boosts, posting fees, and donations.
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
if (fact.type === 'donation') {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.0049 16.941V19.0029H18.0049V21.0029H6.00488V19.0029H11.0049V16.941C7.05857 16.4489 4.00488 13.0825 4.00488 9.00293V3.00293H20.0049V9.00293C20.0049 13.0825 16.9512 16.4489 13.0049 16.941ZM1.00488 5.00293H3.00488V9.00293H1.00488V5.00293ZM21.0049 5.00293H23.0049V9.00293H21.0049V5.00293Z"></path></svg>
|
After Width: | Height: | Size: 372 B |
Loading…
Reference in New Issue