referrals
This commit is contained in:
parent
4282f6724f
commit
41226245c5
|
@ -1,6 +1,6 @@
|
||||||
const PLACEHOLDERS_NUM = 616
|
const PLACEHOLDERS_NUM = 616
|
||||||
|
|
||||||
function interval (when) {
|
export function interval (when) {
|
||||||
switch (when) {
|
switch (when) {
|
||||||
case 'week':
|
case 'week':
|
||||||
return '1 week'
|
return '1 week'
|
||||||
|
@ -15,7 +15,7 @@ function interval (when) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeUnit (when) {
|
export function timeUnit (when) {
|
||||||
switch (when) {
|
switch (when) {
|
||||||
case 'week':
|
case 'week':
|
||||||
case 'month':
|
case 'month':
|
||||||
|
@ -28,7 +28,7 @@ function timeUnit (when) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function withClause (when) {
|
export function withClause (when) {
|
||||||
const ival = interval(when)
|
const ival = interval(when)
|
||||||
const unit = timeUnit(when)
|
const unit = timeUnit(when)
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ function withClause (when) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACKY AF this is a performance enhancement that allows us to use the created_at indices on tables
|
// HACKY AF this is a performance enhancement that allows us to use the created_at indices on tables
|
||||||
function intervalClause (when, table, and) {
|
export function intervalClause (when, table, and) {
|
||||||
if (when === 'forever') {
|
if (when === 'forever') {
|
||||||
return and ? '' : 'TRUE'
|
return and ? '' : 'TRUE'
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ export default {
|
||||||
return await models.$queryRaw(
|
return await models.$queryRaw(
|
||||||
`${withClause(when)}
|
`${withClause(when)}
|
||||||
SELECT time, json_build_array(
|
SELECT time, json_build_array(
|
||||||
json_build_object('name', 'invited', 'value', count("inviteId")),
|
json_build_object('name', 'referrals', 'value', count("referrerId")),
|
||||||
json_build_object('name', 'organic', 'value', count(users.id) FILTER(WHERE id > ${PLACEHOLDERS_NUM}) - count("inviteId"))
|
json_build_object('name', 'organic', 'value', count(users.id) FILTER(WHERE id > ${PLACEHOLDERS_NUM}) - count("inviteId"))
|
||||||
) AS data
|
) AS data
|
||||||
FROM times
|
FROM times
|
||||||
|
@ -131,7 +131,8 @@ export default {
|
||||||
json_build_object('name', 'any', 'value', count(distinct user_id)),
|
json_build_object('name', 'any', 'value', count(distinct user_id)),
|
||||||
json_build_object('name', 'posts', 'value', count(distinct user_id) FILTER (WHERE type = 'POST')),
|
json_build_object('name', 'posts', 'value', count(distinct user_id) FILTER (WHERE type = 'POST')),
|
||||||
json_build_object('name', 'comments', 'value', count(distinct user_id) FILTER (WHERE type = 'COMMENT')),
|
json_build_object('name', 'comments', 'value', count(distinct user_id) FILTER (WHERE type = 'COMMENT')),
|
||||||
json_build_object('name', 'rewards', 'value', count(distinct user_id) FILTER (WHERE type = 'EARN'))
|
json_build_object('name', 'rewards', 'value', count(distinct user_id) FILTER (WHERE type = 'EARN')),
|
||||||
|
json_build_object('name', 'referrals', 'value', count(distinct user_id) FILTER (WHERE type = 'REFERRAL'))
|
||||||
) AS data
|
) AS data
|
||||||
FROM times
|
FROM times
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
|
@ -142,7 +143,11 @@ export default {
|
||||||
UNION ALL
|
UNION ALL
|
||||||
(SELECT created_at, "userId" as user_id, 'EARN' as type
|
(SELECT created_at, "userId" as user_id, 'EARN' as type
|
||||||
FROM "Earn"
|
FROM "Earn"
|
||||||
WHERE ${intervalClause(when, 'Earn', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
|
WHERE ${intervalClause(when, 'Earn', false)})
|
||||||
|
UNION ALL
|
||||||
|
(SELECT created_at, "referrerId" as user_id, 'REFERRAL' as type
|
||||||
|
FROM "ReferralAct"
|
||||||
|
WHERE ${intervalClause(when, 'ReferralAct', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
|
||||||
GROUP BY time
|
GROUP BY time
|
||||||
ORDER BY time ASC`)
|
ORDER BY time ASC`)
|
||||||
},
|
},
|
||||||
|
@ -152,18 +157,24 @@ export default {
|
||||||
SELECT time, json_build_array(
|
SELECT time, json_build_array(
|
||||||
json_build_object('name', 'rewards', 'value', coalesce(floor(sum(airdrop)/1000),0)),
|
json_build_object('name', 'rewards', 'value', coalesce(floor(sum(airdrop)/1000),0)),
|
||||||
json_build_object('name', 'posts', 'value', coalesce(floor(sum(post)/1000),0)),
|
json_build_object('name', 'posts', 'value', coalesce(floor(sum(post)/1000),0)),
|
||||||
json_build_object('name', 'comments', 'value', coalesce(floor(sum(comment)/1000),0))
|
json_build_object('name', 'comments', 'value', coalesce(floor(sum(comment)/1000),0)),
|
||||||
|
json_build_object('name', 'referrals', 'value', coalesce(floor(sum(referral)/1000),0))
|
||||||
) AS data
|
) AS data
|
||||||
FROM times
|
FROM times
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
((SELECT "ItemAct".created_at, 0 as airdrop,
|
((SELECT "ItemAct".created_at, 0 as airdrop,
|
||||||
CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".msats END as comment,
|
CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".msats END as comment,
|
||||||
CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post
|
CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post,
|
||||||
|
0 as referral
|
||||||
FROM "ItemAct"
|
FROM "ItemAct"
|
||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
||||||
WHERE ${intervalClause(when, 'ItemAct', true)} "ItemAct".act = 'TIP')
|
WHERE ${intervalClause(when, 'ItemAct', true)} "ItemAct".act = 'TIP')
|
||||||
UNION ALL
|
UNION ALL
|
||||||
(SELECT created_at, msats as airdrop, 0 as post, 0 as comment
|
(SELECT created_at, 0 as airdrop, 0 as post, 0 as comment, msats as referral
|
||||||
|
FROM "ReferralAct"
|
||||||
|
WHERE ${intervalClause(when, 'ReferralAct', false)})
|
||||||
|
UNION ALL
|
||||||
|
(SELECT created_at, msats as airdrop, 0 as post, 0 as comment, 0 as referral
|
||||||
FROM "Earn"
|
FROM "Earn"
|
||||||
WHERE ${intervalClause(when, 'Earn', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
|
WHERE ${intervalClause(when, 'Earn', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
|
||||||
GROUP BY time
|
GROUP BY time
|
||||||
|
|
|
@ -10,7 +10,8 @@ import upload from './upload'
|
||||||
import growth from './growth'
|
import growth from './growth'
|
||||||
import search from './search'
|
import search from './search'
|
||||||
import rewards from './rewards'
|
import rewards from './rewards'
|
||||||
|
import referrals from './referrals'
|
||||||
import { GraphQLJSONObject } from 'graphql-type-json'
|
import { GraphQLJSONObject } from 'graphql-type-json'
|
||||||
|
|
||||||
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
|
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
|
||||||
upload, growth, search, rewards, { JSONObject: GraphQLJSONObject }]
|
upload, growth, search, rewards, referrals, { JSONObject: GraphQLJSONObject }]
|
||||||
|
|
|
@ -160,6 +160,15 @@ export default {
|
||||||
ORDER BY "sortTime" DESC
|
ORDER BY "sortTime" DESC
|
||||||
LIMIT ${LIMIT}+$3)`
|
LIMIT ${LIMIT}+$3)`
|
||||||
)
|
)
|
||||||
|
queries.push(
|
||||||
|
`(SELECT users.id::text, users.created_at AS "sortTime", NULL as "earnedSats",
|
||||||
|
'Referral' AS type
|
||||||
|
FROM users
|
||||||
|
WHERE "users"."referrerId" = $1
|
||||||
|
AND "inviteId" IS NULL
|
||||||
|
AND users.created_at <= $2
|
||||||
|
LIMIT ${LIMIT}+$3)`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meFull.noteEarning) {
|
if (meFull.noteEarning) {
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { AuthenticationError } from 'apollo-server-micro'
|
||||||
|
import { withClause, intervalClause, timeUnit } from './growth'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Query: {
|
||||||
|
referrals: async (parent, { when }, { models, me }) => {
|
||||||
|
if (!me) {
|
||||||
|
throw new AuthenticationError('you must be logged in')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [{ totalSats }] = await models.$queryRaw(`
|
||||||
|
SELECT COALESCE(FLOOR(sum(msats) / 1000), 0) as "totalSats"
|
||||||
|
FROM "ReferralAct"
|
||||||
|
WHERE ${intervalClause(when, 'ReferralAct', true)}
|
||||||
|
"ReferralAct"."referrerId" = $1
|
||||||
|
`, Number(me.id))
|
||||||
|
|
||||||
|
const [{ totalReferrals }] = await models.$queryRaw(`
|
||||||
|
SELECT count(*) as "totalReferrals"
|
||||||
|
FROM users
|
||||||
|
WHERE ${intervalClause(when, 'users', true)}
|
||||||
|
"referrerId" = $1
|
||||||
|
`, Number(me.id))
|
||||||
|
|
||||||
|
const stats = await models.$queryRaw(
|
||||||
|
`${withClause(when)}
|
||||||
|
SELECT time, json_build_array(
|
||||||
|
json_build_object('name', 'referrals', 'value', count(*) FILTER (WHERE act = 'REFERREE')),
|
||||||
|
json_build_object('name', 'sats', 'value', FLOOR(COALESCE(sum(msats) FILTER (WHERE act IN ('BOOST', 'STREAM', 'FEE')), 0)))
|
||||||
|
) AS data
|
||||||
|
FROM times
|
||||||
|
LEFT JOIN
|
||||||
|
((SELECT "ReferralAct".created_at, "ReferralAct".msats / 1000.0 as msats, "ItemAct".act::text as act
|
||||||
|
FROM "ReferralAct"
|
||||||
|
JOIN "ItemAct" ON "ItemAct".id = "ReferralAct"."itemActId"
|
||||||
|
WHERE ${intervalClause(when, 'ReferralAct', true)}
|
||||||
|
"ReferralAct"."referrerId" = $1)
|
||||||
|
UNION ALL
|
||||||
|
(SELECT created_at, 0.0 as sats, 'REFERREE' as act
|
||||||
|
FROM users
|
||||||
|
WHERE ${intervalClause(when, 'users', true)}
|
||||||
|
"referrerId" = $1)) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
|
||||||
|
GROUP BY time
|
||||||
|
ORDER BY time ASC`, Number(me.id))
|
||||||
|
|
||||||
|
console.log(totalSats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalSats,
|
||||||
|
totalReferrals,
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,18 +12,19 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
const [result] = await models.$queryRaw`
|
const [result] = await models.$queryRaw`
|
||||||
SELECT coalesce(sum(sats), 0) as total, json_build_array(
|
SELECT coalesce(FLOOR(sum(sats)), 0) as total, json_build_array(
|
||||||
json_build_object('name', 'donations', 'value', coalesce(sum(sats) FILTER(WHERE type = 'DONATION'), 0)),
|
json_build_object('name', 'donations', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'DONATION')), 0)),
|
||||||
json_build_object('name', 'fees', 'value', coalesce(sum(sats) FILTER(WHERE type NOT IN ('BOOST', 'STREAM', 'DONATION')), 0)),
|
json_build_object('name', 'fees', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type NOT IN ('BOOST', 'STREAM', 'DONATION'))), 0)),
|
||||||
json_build_object('name', 'boost', 'value', coalesce(sum(sats) FILTER(WHERE type = 'BOOST'), 0)),
|
json_build_object('name', 'boost', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'BOOST')), 0)),
|
||||||
json_build_object('name', 'jobs', 'value', coalesce(sum(sats) FILTER(WHERE type = 'STREAM'), 0))
|
json_build_object('name', 'jobs', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'STREAM')), 0))
|
||||||
) AS sources
|
) AS sources
|
||||||
FROM (
|
FROM (
|
||||||
(SELECT msats / 1000 as sats, act::text as type
|
(SELECT ("ItemAct".msats - COALESCE("ReferralAct".msats, 0)) / 1000.0 as sats, act::text as type
|
||||||
FROM "ItemAct"
|
FROM "ItemAct"
|
||||||
WHERE created_at > ${lastReward.createdAt} AND "ItemAct".act <> 'TIP')
|
LEFT JOIN "ReferralAct" ON "ItemAct".id = "ReferralAct"."itemActId"
|
||||||
|
WHERE "ItemAct".created_at > ${lastReward.createdAt} AND "ItemAct".act <> 'TIP')
|
||||||
UNION ALL
|
UNION ALL
|
||||||
(SELECT sats, 'DONATION' as type
|
(SELECT sats::FLOAT, 'DONATION' as type
|
||||||
FROM "Donation"
|
FROM "Donation"
|
||||||
WHERE created_at > ${lastReward.createdAt})
|
WHERE created_at > ${lastReward.createdAt})
|
||||||
) subquery`
|
) subquery`
|
||||||
|
|
|
@ -271,6 +271,18 @@ export default {
|
||||||
if (newInvitees.length > 0) {
|
if (newInvitees.length > 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const referral = await models.user.findFirst({
|
||||||
|
where: {
|
||||||
|
referrerId: me.id,
|
||||||
|
createdAt: {
|
||||||
|
gt: lastChecked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (referral) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||||
import lnpr from 'bolt11'
|
import lnpr from 'bolt11'
|
||||||
import { SELECT } from './item'
|
import { SELECT } from './item'
|
||||||
import { lnurlPayDescriptionHash } from '../../lib/lnurl'
|
import { lnurlPayDescriptionHash } from '../../lib/lnurl'
|
||||||
import { msatsToSats } from '../../lib/format'
|
import { msatsToSats, msatsToSatsDecimal } from '../../lib/format'
|
||||||
|
|
||||||
export async function getInvoice (parent, { id }, { me, models }) {
|
export async function getInvoice (parent, { id }, { me, models }) {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
|
@ -110,6 +110,12 @@ export default {
|
||||||
FROM "Earn"
|
FROM "Earn"
|
||||||
WHERE "Earn"."userId" = $1 AND "Earn".created_at <= $2
|
WHERE "Earn"."userId" = $1 AND "Earn".created_at <= $2
|
||||||
GROUP BY "userId", created_at)`)
|
GROUP BY "userId", created_at)`)
|
||||||
|
queries.push(
|
||||||
|
`(SELECT ('referral' || "ReferralAct".id) as id, "ReferralAct".id as "factId", NULL as bolt11,
|
||||||
|
created_at as "createdAt", msats,
|
||||||
|
0 as "msatsFee", NULL as status, 'referral' as type
|
||||||
|
FROM "ReferralAct"
|
||||||
|
WHERE "ReferralAct"."referrerId" = $1 AND "ReferralAct".created_at <= $2)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include.has('spent')) {
|
if (include.has('spent')) {
|
||||||
|
@ -287,8 +293,8 @@ export default {
|
||||||
|
|
||||||
return item
|
return item
|
||||||
},
|
},
|
||||||
sats: fact => msatsToSats(fact.msats),
|
sats: fact => msatsToSatsDecimal(fact.msats),
|
||||||
satsFee: fact => msatsToSats(fact.msatsFee)
|
satsFee: fact => msatsToSatsDecimal(fact.msatsFee)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import sub from './sub'
|
||||||
import upload from './upload'
|
import upload from './upload'
|
||||||
import growth from './growth'
|
import growth from './growth'
|
||||||
import rewards from './rewards'
|
import rewards from './rewards'
|
||||||
|
import referrals from './referrals'
|
||||||
|
|
||||||
const link = gql`
|
const link = gql`
|
||||||
type Query {
|
type Query {
|
||||||
|
@ -27,4 +28,4 @@ const link = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
export default [link, user, item, message, wallet, lnurl, notifications, invite,
|
export default [link, user, item, message, wallet, lnurl, notifications, invite,
|
||||||
sub, upload, growth, rewards]
|
sub, upload, growth, rewards, referrals]
|
||||||
|
|
|
@ -50,8 +50,12 @@ export default gql`
|
||||||
sortTime: String!
|
sortTime: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Referral {
|
||||||
|
sortTime: String!
|
||||||
|
}
|
||||||
|
|
||||||
union Notification = Reply | Votification | Mention
|
union Notification = Reply | Votification | Mention
|
||||||
| Invitification | Earn | JobChanged | InvoicePaid
|
| Invitification | Earn | JobChanged | InvoicePaid | Referral
|
||||||
|
|
||||||
type Notifications {
|
type Notifications {
|
||||||
lastChecked: String
|
lastChecked: String
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { gql } from 'apollo-server-micro'
|
||||||
|
|
||||||
|
export default gql`
|
||||||
|
extend type Query {
|
||||||
|
referrals(when: String): Referrals!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Referrals {
|
||||||
|
totalSats: Int!
|
||||||
|
totalReferrals: Int!
|
||||||
|
stats: [TimeData!]!
|
||||||
|
}
|
||||||
|
`
|
|
@ -41,8 +41,8 @@ export default gql`
|
||||||
factId: ID!
|
factId: ID!
|
||||||
bolt11: String
|
bolt11: String
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
sats: Int!
|
sats: Float!
|
||||||
satsFee: Int
|
satsFee: Float
|
||||||
status: String
|
status: String
|
||||||
type: String!
|
type: String!
|
||||||
description: String
|
description: String
|
||||||
|
|
|
@ -18,6 +18,7 @@ import DontLikeThis from './dont-link-this'
|
||||||
import Flag from '../svgs/flag-fill.svg'
|
import Flag from '../svgs/flag-fill.svg'
|
||||||
import { Badge } from 'react-bootstrap'
|
import { Badge } from 'react-bootstrap'
|
||||||
import { abbrNum } from '../lib/format'
|
import { abbrNum } from '../lib/format'
|
||||||
|
import Share from './share'
|
||||||
|
|
||||||
function Parent ({ item, rootText }) {
|
function Parent ({ item, rootText }) {
|
||||||
const ParentFrag = () => (
|
const ParentFrag = () => (
|
||||||
|
@ -169,6 +170,7 @@ export default function Comment ({
|
||||||
localStorage.setItem(`commentCollapse:${item.id}`, 'yep')
|
localStorage.setItem(`commentCollapse:${item.id}`, 'yep')
|
||||||
}}
|
}}
|
||||||
/>)}
|
/>)}
|
||||||
|
{topLevel && <Share item={item} />}
|
||||||
</div>
|
</div>
|
||||||
{edit
|
{edit
|
||||||
? (
|
? (
|
||||||
|
|
|
@ -92,13 +92,8 @@ export default function Header ({ sub }) {
|
||||||
<NavDropdown.Item eventKey='satistics'>satistics</NavDropdown.Item>
|
<NavDropdown.Item eventKey='satistics'>satistics</NavDropdown.Item>
|
||||||
</Link>
|
</Link>
|
||||||
<NavDropdown.Divider />
|
<NavDropdown.Divider />
|
||||||
<Link href='/invites' passHref>
|
<Link href='/referrals/month' passHref>
|
||||||
<NavDropdown.Item eventKey='invites'>invites
|
<NavDropdown.Item eventKey='referrals'>referrals</NavDropdown.Item>
|
||||||
{me && !me.hasInvites &&
|
|
||||||
<div className='p-1 d-inline-block bg-success ml-1'>
|
|
||||||
<span className='invisible'>{' '}</span>
|
|
||||||
</div>}
|
|
||||||
</NavDropdown.Item>
|
|
||||||
</Link>
|
</Link>
|
||||||
<NavDropdown.Divider />
|
<NavDropdown.Divider />
|
||||||
<div className='d-flex align-items-center'>
|
<div className='d-flex align-items-center'>
|
||||||
|
|
|
@ -84,7 +84,7 @@ export function ItemActModal () {
|
||||||
{[1, 10, 100, 1000, 10000].map(num =>
|
{[1, 10, 100, 1000, 10000].map(num =>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
className={num > 1 ? 'ml-2' : ''}
|
className={`${num > 1 ? 'ml-2' : ''} mb-2`}
|
||||||
key={num}
|
key={num}
|
||||||
onClick={() => { setOValue(num) }}
|
onClick={() => { setOValue(num) }}
|
||||||
>
|
>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { newComments } from '../lib/new-comments'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import DontLikeThis from './dont-link-this'
|
import DontLikeThis from './dont-link-this'
|
||||||
import Flag from '../svgs/flag-fill.svg'
|
import Flag from '../svgs/flag-fill.svg'
|
||||||
|
import Share from './share'
|
||||||
import { abbrNum } from '../lib/format'
|
import { abbrNum } from '../lib/format'
|
||||||
|
|
||||||
export function SearchTitle ({ title }) {
|
export function SearchTitle ({ title }) {
|
||||||
|
@ -141,7 +142,11 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
|
||||||
</div>
|
</div>
|
||||||
{showFwdUser && item.fwdUser && <FwdUser user={item.fwdUser} />}
|
{showFwdUser && item.fwdUser && <FwdUser user={item.fwdUser} />}
|
||||||
</div>
|
</div>
|
||||||
{toc && <Toc text={item.text} />}
|
{toc &&
|
||||||
|
<>
|
||||||
|
<Share item={item} />
|
||||||
|
<Toc text={item.text} />
|
||||||
|
</>}
|
||||||
</div>
|
</div>
|
||||||
{children && (
|
{children && (
|
||||||
<div className={styles.children}>
|
<div className={styles.children}>
|
||||||
|
|
|
@ -20,7 +20,7 @@ function Notification ({ n }) {
|
||||||
<div
|
<div
|
||||||
className='clickToContext'
|
className='clickToContext'
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
if (n.__typename === 'Earn') {
|
if (n.__typename === 'Earn' || n.__typename === 'Referral') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,41 +88,50 @@ function Notification ({ n }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: n.__typename === 'InvoicePaid'
|
: n.__typename === 'Referral'
|
||||||
? (
|
? (
|
||||||
<div className='font-weight-bold text-info ml-2 py-1'>
|
|
||||||
<Check className='fill-info mr-1' />{n.earnedSats} sats were deposited in your account
|
|
||||||
<small className='text-muted ml-1'>{timeSince(new Date(n.sortTime))}</small>
|
|
||||||
</div>)
|
|
||||||
: (
|
|
||||||
<>
|
<>
|
||||||
{n.__typename === 'Votification' &&
|
<small className='font-weight-bold text-secondary ml-2'>
|
||||||
<small className='font-weight-bold text-success ml-2'>
|
someone joined via one of your <Link href='/referrals/month' passHref><a className='text-reset'>referral links</a></Link>
|
||||||
your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
|
<small className='text-muted ml-1'>{timeSince(new Date(n.sortTime))}</small>
|
||||||
</small>}
|
</small>
|
||||||
{n.__typename === 'Mention' &&
|
</>
|
||||||
<small className='font-weight-bold text-info ml-2'>
|
)
|
||||||
you were mentioned in
|
: n.__typename === 'InvoicePaid'
|
||||||
</small>}
|
? (
|
||||||
{n.__typename === 'JobChanged' &&
|
<div className='font-weight-bold text-info ml-2 py-1'>
|
||||||
<small className={`font-weight-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ml-1`}>
|
<Check className='fill-info mr-1' />{n.earnedSats} sats were deposited in your account
|
||||||
{n.item.status === 'ACTIVE'
|
<small className='text-muted ml-1'>{timeSince(new Date(n.sortTime))}</small>
|
||||||
? 'your job is active again'
|
</div>)
|
||||||
: (n.item.status === 'NOSATS'
|
: (
|
||||||
? 'your job promotion ran out of sats'
|
<>
|
||||||
: 'your job has been stopped')}
|
{n.__typename === 'Votification' &&
|
||||||
</small>}
|
<small className='font-weight-bold text-success ml-2'>
|
||||||
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
|
your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
|
||||||
{n.item.isJob
|
</small>}
|
||||||
? <ItemJob item={n.item} />
|
{n.__typename === 'Mention' &&
|
||||||
: n.item.title
|
<small className='font-weight-bold text-info ml-2'>
|
||||||
? <Item item={n.item} />
|
you were mentioned in
|
||||||
: (
|
</small>}
|
||||||
<div className='pb-2'>
|
{n.__typename === 'JobChanged' &&
|
||||||
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
|
<small className={`font-weight-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ml-1`}>
|
||||||
</div>)}
|
{n.item.status === 'ACTIVE'
|
||||||
</div>
|
? 'your job is active again'
|
||||||
</>)}
|
: (n.item.status === 'NOSATS'
|
||||||
|
? 'your job promotion ran out of sats'
|
||||||
|
: 'your job has been stopped')}
|
||||||
|
</small>}
|
||||||
|
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
|
||||||
|
{n.item.isJob
|
||||||
|
? <ItemJob item={n.item} />
|
||||||
|
: n.item.title
|
||||||
|
? <Item item={n.item} />
|
||||||
|
: (
|
||||||
|
<div className='pb-2'>
|
||||||
|
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
|
||||||
|
</div>)}
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
import ShareIcon from '../svgs/share-fill.svg'
|
||||||
|
import copy from 'clipboard-copy'
|
||||||
|
import { useMe } from './me'
|
||||||
|
|
||||||
|
export default function Share ({ item }) {
|
||||||
|
const me = useMe()
|
||||||
|
const url = `https://stacker.news/items/${item.id}${me ? `/r/${me.name}` : ''}`
|
||||||
|
|
||||||
|
return typeof window !== 'undefined' && navigator?.share
|
||||||
|
? (
|
||||||
|
<div className='d-flex align-items-center'>
|
||||||
|
<ShareIcon
|
||||||
|
onClick={() => {
|
||||||
|
if (navigator.share) {
|
||||||
|
navigator.share({
|
||||||
|
title: item.title || '',
|
||||||
|
text: '',
|
||||||
|
url
|
||||||
|
}).then(() => console.log('Successful share'))
|
||||||
|
.catch((error) => console.log('Error sharing', error))
|
||||||
|
} else {
|
||||||
|
console.log('no navigator.share')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>)
|
||||||
|
: (
|
||||||
|
<Dropdown alignRight className='pointer d-flex align-items-center' as='span'>
|
||||||
|
<Dropdown.Toggle variant='success' id='dropdown-basic' as='a'>
|
||||||
|
<ShareIcon className='mx-2 fill-grey theme' />
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Item
|
||||||
|
className='text-center'
|
||||||
|
onClick={async () => {
|
||||||
|
copy(url)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
copy link
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>)
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, AreaChart, Area, ComposedChart, Bar } from 'recharts'
|
||||||
|
import { abbrNum } from '../lib/format'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
const dateFormatter = when => {
|
||||||
|
return timeStr => {
|
||||||
|
const date = new Date(timeStr)
|
||||||
|
switch (when) {
|
||||||
|
case 'week':
|
||||||
|
case 'month':
|
||||||
|
return `${('0' + (date.getUTCMonth() % 12 + 1)).slice(-2)}/${date.getUTCDate()}`
|
||||||
|
case 'year':
|
||||||
|
case 'forever':
|
||||||
|
return `${('0' + (date.getUTCMonth() % 12 + 1)).slice(-2)}/${String(date.getUTCFullYear()).slice(-2)}`
|
||||||
|
default:
|
||||||
|
return `${date.getHours() % 12 || 12}${date.getHours() >= 12 ? 'pm' : 'am'}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function xAxisName (when) {
|
||||||
|
switch (when) {
|
||||||
|
case 'week':
|
||||||
|
case 'month':
|
||||||
|
return 'days'
|
||||||
|
case 'year':
|
||||||
|
case 'forever':
|
||||||
|
return 'months'
|
||||||
|
default:
|
||||||
|
return 'hours'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformData = data => {
|
||||||
|
return data.map(entry => {
|
||||||
|
const obj = { time: entry.time }
|
||||||
|
entry.data.forEach(entry1 => {
|
||||||
|
obj[entry1.name] = entry1.value
|
||||||
|
})
|
||||||
|
return obj
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
'var(--secondary)',
|
||||||
|
'var(--info)',
|
||||||
|
'var(--success)',
|
||||||
|
'var(--boost)',
|
||||||
|
'var(--theme-grey)',
|
||||||
|
'var(--danger)'
|
||||||
|
]
|
||||||
|
|
||||||
|
export function WhenAreaChart ({ data }) {
|
||||||
|
const router = useRouter()
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// transform data into expected shape
|
||||||
|
data = transformData(data)
|
||||||
|
// need to grab when
|
||||||
|
const when = router.query.when
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width='100%' height={300} minWidth={300}>
|
||||||
|
<AreaChart
|
||||||
|
data={data}
|
||||||
|
margin={{
|
||||||
|
top: 5,
|
||||||
|
right: 5,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XAxis
|
||||||
|
dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
|
||||||
|
tick={{ fill: 'var(--theme-grey)' }}
|
||||||
|
/>
|
||||||
|
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
||||||
|
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
|
||||||
|
<Legend />
|
||||||
|
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
|
||||||
|
<Area key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WhenLineChart ({ data }) {
|
||||||
|
const router = useRouter()
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// transform data into expected shape
|
||||||
|
data = transformData(data)
|
||||||
|
// need to grab when
|
||||||
|
const when = router.query.when
|
||||||
|
|
||||||
|
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(when)} name={xAxisName(when)}
|
||||||
|
tick={{ fill: 'var(--theme-grey)' }}
|
||||||
|
/>
|
||||||
|
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
||||||
|
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
|
||||||
|
<Legend />
|
||||||
|
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
|
||||||
|
<Line key={v} type='monotone' dataKey={v} name={v} stroke={COLORS[i]} fill={COLORS[i]} />)}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WhenComposedChart ({ data, lineNames, areaNames, barNames }) {
|
||||||
|
const router = useRouter()
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// transform data into expected shape
|
||||||
|
data = transformData(data)
|
||||||
|
// need to grab when
|
||||||
|
const when = router.query.when
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width='100%' height={300} minWidth={300}>
|
||||||
|
<ComposedChart
|
||||||
|
data={data}
|
||||||
|
margin={{
|
||||||
|
top: 5,
|
||||||
|
right: 5,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XAxis
|
||||||
|
dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
|
||||||
|
tick={{ fill: 'var(--theme-grey)' }}
|
||||||
|
/>
|
||||||
|
<YAxis yAxisId='left' orientation='left' allowDecimals={false} stroke='var(--theme-grey)' tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
||||||
|
<YAxis yAxisId='right' orientation='right' allowDecimals={false} stroke='var(--theme-grey)' tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
||||||
|
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
|
||||||
|
<Legend />
|
||||||
|
{barNames?.map((v, i) =>
|
||||||
|
<Bar yAxisId='right' key={v} type='monotone' dataKey={v} name={v} stroke='var(--info)' fill='var(--info)' />)}
|
||||||
|
{areaNames?.map((v, i) =>
|
||||||
|
<Area yAxisId='left' key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
|
||||||
|
{lineNames?.map((v, i) =>
|
||||||
|
<Line yAxisId='left' key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
|
||||||
|
</ComposedChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
|
@ -37,6 +37,9 @@ export const NOTIFICATIONS = gql`
|
||||||
tips
|
tips
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
... on Referral {
|
||||||
|
sortTime
|
||||||
|
}
|
||||||
... on Reply {
|
... on Reply {
|
||||||
sortTime
|
sortTime
|
||||||
item {
|
item {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const NOFOLLOW_LIMIT = 1000
|
export const NOFOLLOW_LIMIT = 100
|
||||||
export const BOOST_MIN = 5000
|
export const BOOST_MIN = 5000
|
||||||
export const UPLOAD_SIZE_MAX = 2 * 1024 * 1024
|
export const UPLOAD_SIZE_MAX = 2 * 1024 * 1024
|
||||||
export const IMAGE_PIXELS_MAX = 35000000
|
export const IMAGE_PIXELS_MAX = 35000000
|
||||||
|
|
|
@ -16,3 +16,10 @@ export const msatsToSats = msats => {
|
||||||
}
|
}
|
||||||
return Number(BigInt(msats) / 1000n)
|
return Number(BigInt(msats) / 1000n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const msatsToSatsDecimal = msats => {
|
||||||
|
if (msats === null || msats === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return fixedDecimal(msats / 1000.0, 3)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export function middleware (request) {
|
||||||
|
const regex = /(\/.*)?\/r\/([\w_]+)/
|
||||||
|
const m = regex.exec(request.nextUrl.pathname)
|
||||||
|
|
||||||
|
const url = new URL(m[1] || '/', request.url)
|
||||||
|
url.search = request.nextUrl.search
|
||||||
|
url.hash = request.nextUrl.hash
|
||||||
|
|
||||||
|
const resp = NextResponse.redirect(url)
|
||||||
|
resp.cookies.set('sn_referrer', m[2])
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ['/(.*/|)r/([\\w_]+)([?#]?.*)']
|
||||||
|
}
|
|
@ -5,9 +5,7 @@ import prisma from '../../../api/models'
|
||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
import { getSession } from 'next-auth/client'
|
import { getSession } from 'next-auth/client'
|
||||||
|
|
||||||
export default (req, res) => NextAuth(req, res, options)
|
export default (req, res) => NextAuth(req, res, {
|
||||||
|
|
||||||
const options = {
|
|
||||||
callbacks: {
|
callbacks: {
|
||||||
/**
|
/**
|
||||||
* @param {object} token Decrypted JSON Web Token
|
* @param {object} token Decrypted JSON Web Token
|
||||||
|
@ -26,22 +24,32 @@ const options = {
|
||||||
token.user = { id: Number(user.id) }
|
token.user = { id: Number(user.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// sign them up for the newsletter
|
if (isNewUser) {
|
||||||
if (isNewUser && profile.email) {
|
// if referrer exists, set on user
|
||||||
fetch(process.env.LIST_MONK_URL + '/api/subscribers', {
|
if (req.cookies.sn_referrer && user?.id) {
|
||||||
method: 'POST',
|
const referrer = await prisma.user.findUnique({ where: { name: req.cookies.sn_referrer } })
|
||||||
headers: {
|
if (referrer) {
|
||||||
'Content-Type': 'application/json',
|
await prisma.user.update({ where: { id: user.id }, data: { referrerId: referrer.id } })
|
||||||
Authorization: 'Basic ' + Buffer.from(process.env.LIST_MONK_AUTH).toString('base64')
|
}
|
||||||
},
|
}
|
||||||
body: JSON.stringify({
|
|
||||||
email: profile.email,
|
// sign them up for the newsletter
|
||||||
name: 'blank',
|
if (profile.email) {
|
||||||
lists: [2],
|
fetch(process.env.LIST_MONK_URL + '/api/subscribers', {
|
||||||
status: 'enabled',
|
method: 'POST',
|
||||||
preconfirm_subscriptions: true
|
headers: {
|
||||||
})
|
'Content-Type': 'application/json',
|
||||||
}).then(async r => console.log(await r.json())).catch(console.log)
|
Authorization: 'Basic ' + Buffer.from(process.env.LIST_MONK_AUTH).toString('base64')
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: profile.email,
|
||||||
|
name: 'blank',
|
||||||
|
lists: [2],
|
||||||
|
status: 'enabled',
|
||||||
|
preconfirm_subscriptions: true
|
||||||
|
})
|
||||||
|
}).then(async r => console.log(await r.json())).catch(console.log)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return token
|
return token
|
||||||
|
@ -130,7 +138,7 @@ const options = {
|
||||||
pages: {
|
pages: {
|
||||||
signIn: '/login'
|
signIn: '/login'
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
function sendVerificationRequest ({
|
function sendVerificationRequest ({
|
||||||
identifier: email,
|
identifier: email,
|
||||||
|
|
|
@ -120,7 +120,7 @@ export default function Invites () {
|
||||||
<h2 className='mt-3 mb-0'>
|
<h2 className='mt-3 mb-0'>
|
||||||
invite links
|
invite links
|
||||||
</h2>
|
</h2>
|
||||||
<small className='d-block text-muted font-weight-bold mx-5'>send these to people you trust somewhat, e.g. group chats or DMs</small>
|
<small className='d-block text-muted font-weight-bold mx-5'>send these to people you trust, e.g. group chats or DMs</small>
|
||||||
</div>
|
</div>
|
||||||
<InviteForm />
|
<InviteForm />
|
||||||
{active.length > 0 && <InviteList name='active' invites={active} />}
|
{active.length > 0 && <InviteList name='active' invites={active} />}
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { gql } from 'apollo-server-micro'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { getGetServerSideProps } from '../../api/ssrApollo'
|
||||||
|
import { CopyInput, Form, Select } from '../../components/form'
|
||||||
|
import LayoutCenter from '../../components/layout-center'
|
||||||
|
import { useMe } from '../../components/me'
|
||||||
|
import { WhenComposedChart } from '../../components/when-charts'
|
||||||
|
|
||||||
|
export const getServerSideProps = getGetServerSideProps(
|
||||||
|
gql`
|
||||||
|
query Referrals($when: String!)
|
||||||
|
{
|
||||||
|
referrals(when: $when) {
|
||||||
|
totalSats
|
||||||
|
totalReferrals
|
||||||
|
stats {
|
||||||
|
time
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
export default function Referrals ({ data: { referrals: { totalSats, totalReferrals, stats } } }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const me = useMe()
|
||||||
|
return (
|
||||||
|
<LayoutCenter footerLinks>
|
||||||
|
<Form
|
||||||
|
initial={{
|
||||||
|
when: router.query.when
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h4 className='font-weight-bold text-muted text-center pt-5 pb-3 d-flex align-items-center justify-content-center'>
|
||||||
|
{totalReferrals} referrals & {totalSats} sats in the last
|
||||||
|
<Select
|
||||||
|
groupClassName='mb-0 ml-2'
|
||||||
|
className='w-auto'
|
||||||
|
name='when'
|
||||||
|
size='sm'
|
||||||
|
items={['day', 'week', 'month', 'year', 'forever']}
|
||||||
|
onChange={(formik, e) => router.push(`/referrals/${e.target.value}`)}
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
|
</Form>
|
||||||
|
<WhenComposedChart data={stats} lineNames={['sats']} barNames={['referrals']} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className='text-small pt-5 px-3 d-flex w-100 align-items-center'
|
||||||
|
>
|
||||||
|
<div className='nav-item text-muted pr-2' style={{ 'white-space': 'nowrap' }}>referral link:</div>
|
||||||
|
<CopyInput
|
||||||
|
size='sm'
|
||||||
|
groupClassName='mb-0 w-100'
|
||||||
|
readOnly
|
||||||
|
noForm
|
||||||
|
placeholder={`https://stacker.news/r/${me.name}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ul className='py-3 text-muted'>
|
||||||
|
<li>{`appending /r/${me.name} to any SN link makes it a ref link`}
|
||||||
|
<ul>
|
||||||
|
<li>e.g. https://stacker.news/items/1/r/{me.name}</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>earn 21% of boost and job fees spent by referred stackers</li>
|
||||||
|
<li>earn 2.1% of all tips received by referred stackers</li>
|
||||||
|
<li><Link href='/invites' passHref><a>invite links</a></Link> are also implicitly referral links</li>
|
||||||
|
</ul>
|
||||||
|
</LayoutCenter>
|
||||||
|
)
|
||||||
|
}
|
|
@ -15,7 +15,6 @@ import { useRouter } from 'next/router'
|
||||||
import Item from '../components/item'
|
import Item from '../components/item'
|
||||||
import Comment from '../components/comment'
|
import Comment from '../components/comment'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Info from '../components/info'
|
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps(WALLET_HISTORY)
|
export const getServerSideProps = getGetServerSideProps(WALLET_HISTORY)
|
||||||
|
|
||||||
|
@ -102,7 +101,16 @@ function Detail ({ fact }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={satusClass(fact.status)}>
|
<div className={satusClass(fact.status)}>
|
||||||
You made a donation to daily rewards!
|
You made a donation to <Link href='/rewards' passHref><a>daily rewards</a></Link>!
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (fact.type === 'referral') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={satusClass(fact.status)}>
|
||||||
|
You stacked sats from <Link href='/referrals/month' passHref><a>a referral</a></Link>!
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -156,6 +164,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor
|
||||||
return `/${fact.type}s/${fact.factId}`
|
return `/${fact.type}s/${fact.factId}`
|
||||||
case 'earn':
|
case 'earn':
|
||||||
case 'donation':
|
case 'donation':
|
||||||
|
case 'referral':
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
return `/items/${fact.factId}`
|
return `/items/${fact.factId}`
|
||||||
|
@ -212,11 +221,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor
|
||||||
<th className={styles.type}>type</th>
|
<th className={styles.type}>type</th>
|
||||||
<th>detail</th>
|
<th>detail</th>
|
||||||
<th className={styles.sats}>
|
<th className={styles.sats}>
|
||||||
<div>sats
|
sats
|
||||||
<Info>
|
|
||||||
<div className='font-weight-bold'>Sats are rounded down from millisats to the nearest sat, so the actual amount might be slightly larger.</div>
|
|
||||||
</Info>
|
|
||||||
</div>
|
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -231,7 +236,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor
|
||||||
<td className={styles.description}>
|
<td className={styles.description}>
|
||||||
<Detail fact={f} />
|
<Detail fact={f} />
|
||||||
</td>
|
</td>
|
||||||
<td className={`${styles.sats} ${satusClass(f.status)}`}>{Math.floor(f.sats)}</td>
|
<td className={`${styles.sats} ${satusClass(f.status)}`}>{f.sats}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
|
|
|
@ -146,7 +146,7 @@ export default function Settings ({ data: { settings } }) {
|
||||||
groupClassName='mb-0'
|
groupClassName='mb-0'
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label='my invite links are redeemed'
|
label='someone joins using my invite or referral links'
|
||||||
name='noteInvites'
|
name='noteInvites'
|
||||||
groupClassName='mb-0'
|
groupClassName='mb-0'
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { gql } from '@apollo/client'
|
import { gql } from '@apollo/client'
|
||||||
import { getGetServerSideProps } from '../../api/ssrApollo'
|
import { getGetServerSideProps } from '../../api/ssrApollo'
|
||||||
import Layout from '../../components/layout'
|
import Layout from '../../components/layout'
|
||||||
import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, AreaChart, Area } from 'recharts'
|
|
||||||
import { Col, Row } from 'react-bootstrap'
|
import { Col, Row } from 'react-bootstrap'
|
||||||
import { abbrNum } from '../../lib/format'
|
|
||||||
import { UsageHeader } from '../../components/usage-header'
|
import { UsageHeader } from '../../components/usage-header'
|
||||||
import { useRouter } from 'next/router'
|
import { WhenLineChart, WhenAreaChart } from '../../components/when-charts'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps(
|
export const getServerSideProps = getGetServerSideProps(
|
||||||
gql`
|
gql`
|
||||||
|
@ -55,46 +53,6 @@ export const getServerSideProps = getGetServerSideProps(
|
||||||
}
|
}
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
// todo: this needs to accomodate hours, days, months now
|
|
||||||
const dateFormatter = when => {
|
|
||||||
return timeStr => {
|
|
||||||
const date = new Date(timeStr)
|
|
||||||
switch (when) {
|
|
||||||
case 'week':
|
|
||||||
case 'month':
|
|
||||||
return `${('0' + (date.getUTCMonth() % 12 + 1)).slice(-2)}/${date.getUTCDate()}`
|
|
||||||
case 'year':
|
|
||||||
case 'forever':
|
|
||||||
return `${('0' + (date.getUTCMonth() % 12 + 1)).slice(-2)}/${String(date.getUTCFullYear()).slice(-2)}`
|
|
||||||
default:
|
|
||||||
return `${date.getHours() % 12 || 12}${date.getHours() >= 12 ? 'pm' : 'am'}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function xAxisName (when) {
|
|
||||||
switch (when) {
|
|
||||||
case 'week':
|
|
||||||
case 'month':
|
|
||||||
return 'days'
|
|
||||||
case 'year':
|
|
||||||
case 'forever':
|
|
||||||
return 'months'
|
|
||||||
default:
|
|
||||||
return 'hours'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const transformData = data => {
|
|
||||||
return data.map(entry => {
|
|
||||||
const obj = { time: entry.time }
|
|
||||||
entry.data.forEach(entry1 => {
|
|
||||||
obj[entry1.name] = entry1.value
|
|
||||||
})
|
|
||||||
return obj
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Growth ({
|
export default function Growth ({
|
||||||
data: { registrationGrowth, itemGrowth, spendingGrowth, spenderGrowth, stackingGrowth, stackerGrowth }
|
data: { registrationGrowth, itemGrowth, spendingGrowth, spenderGrowth, stackingGrowth, stackerGrowth }
|
||||||
}) {
|
}) {
|
||||||
|
@ -104,112 +62,33 @@ export default function Growth ({
|
||||||
<Row>
|
<Row>
|
||||||
<Col className='mt-3'>
|
<Col className='mt-3'>
|
||||||
<div className='text-center text-muted font-weight-bold'>stackers</div>
|
<div className='text-center text-muted font-weight-bold'>stackers</div>
|
||||||
<GrowthLineChart data={stackerGrowth} />
|
<WhenLineChart data={stackerGrowth} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col className='mt-3'>
|
<Col className='mt-3'>
|
||||||
<div className='text-center text-muted font-weight-bold'>stacking</div>
|
<div className='text-center text-muted font-weight-bold'>stacking</div>
|
||||||
<GrowthAreaChart data={stackingGrowth} />
|
<WhenAreaChart data={stackingGrowth} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col className='mt-3'>
|
<Col className='mt-3'>
|
||||||
<div className='text-center text-muted font-weight-bold'>spenders</div>
|
<div className='text-center text-muted font-weight-bold'>spenders</div>
|
||||||
<GrowthLineChart data={spenderGrowth} />
|
<WhenLineChart data={spenderGrowth} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col className='mt-3'>
|
<Col className='mt-3'>
|
||||||
<div className='text-center text-muted font-weight-bold'>spending</div>
|
<div className='text-center text-muted font-weight-bold'>spending</div>
|
||||||
<GrowthAreaChart data={spendingGrowth} />
|
<WhenAreaChart data={spendingGrowth} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col className='mt-3'>
|
<Col className='mt-3'>
|
||||||
<div className='text-center text-muted font-weight-bold'>registrations</div>
|
<div className='text-center text-muted font-weight-bold'>registrations</div>
|
||||||
<GrowthAreaChart data={registrationGrowth} />
|
<WhenAreaChart data={registrationGrowth} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col className='mt-3'>
|
<Col className='mt-3'>
|
||||||
<div className='text-center text-muted font-weight-bold'>items</div>
|
<div className='text-center text-muted font-weight-bold'>items</div>
|
||||||
<GrowthAreaChart data={itemGrowth} />
|
<WhenAreaChart data={itemGrowth} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLORS = [
|
|
||||||
'var(--secondary)',
|
|
||||||
'var(--info)',
|
|
||||||
'var(--success)',
|
|
||||||
'var(--boost)',
|
|
||||||
'var(--theme-grey)',
|
|
||||||
'var(--danger)'
|
|
||||||
]
|
|
||||||
|
|
||||||
function GrowthAreaChart ({ data }) {
|
|
||||||
const router = useRouter()
|
|
||||||
if (!data || data.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
// transform data into expected shape
|
|
||||||
data = transformData(data)
|
|
||||||
// need to grab when
|
|
||||||
const when = router.query.when
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width='100%' height={300} minWidth={300}>
|
|
||||||
<AreaChart
|
|
||||||
data={data}
|
|
||||||
margin={{
|
|
||||||
top: 5,
|
|
||||||
right: 5,
|
|
||||||
left: 0,
|
|
||||||
bottom: 0
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XAxis
|
|
||||||
dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
|
|
||||||
tick={{ fill: 'var(--theme-grey)' }}
|
|
||||||
/>
|
|
||||||
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
|
||||||
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
|
|
||||||
<Legend />
|
|
||||||
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
|
|
||||||
<Area key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function GrowthLineChart ({ data }) {
|
|
||||||
const router = useRouter()
|
|
||||||
if (!data || data.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
// transform data into expected shape
|
|
||||||
data = transformData(data)
|
|
||||||
// need to grab when
|
|
||||||
const when = router.query.when
|
|
||||||
|
|
||||||
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(when)} name={xAxisName(when)}
|
|
||||||
tick={{ fill: 'var(--theme-grey)' }}
|
|
||||||
/>
|
|
||||||
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
|
|
||||||
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
|
|
||||||
<Legend />
|
|
||||||
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
|
|
||||||
<Line key={v} type='monotone' dataKey={v} name={v} stroke={COLORS[i]} fill={COLORS[i]} />)}
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "referrerId" INTEGER;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "users" ADD FOREIGN KEY ("referrerId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ReferralAct" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"referrerId" INTEGER NOT NULL,
|
||||||
|
"itemActId" INTEGER NOT NULL,
|
||||||
|
"msats" BIGINT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ReferralAct" ADD FOREIGN KEY ("referrerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ReferralAct" ADD FOREIGN KEY ("itemActId") REFERENCES "ItemAct"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -0,0 +1,202 @@
|
||||||
|
CREATE OR REPLACE FUNCTION referral_act(referrer_id INTEGER, item_act_id INTEGER)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
act_msats BIGINT;
|
||||||
|
referral_act "ItemActType";
|
||||||
|
referral_msats BIGINT;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
SELECT msats, act INTO act_msats, referral_act FROM "ItemAct" WHERE id = item_act_id;
|
||||||
|
|
||||||
|
IF referral_act IN ('FEE', 'BOOST', 'STREAM') THEN
|
||||||
|
referral_msats := CEIL(act_msats * .21);
|
||||||
|
INSERT INTO "ReferralAct" ("referrerId", "itemActId", msats, created_at, updated_at)
|
||||||
|
VALUES(referrer_id, item_act_id, referral_msats, now_utc(), now_utc());
|
||||||
|
UPDATE users
|
||||||
|
SET msats = msats + referral_msats, "stackedMsats" = "stackedMsats" + referral_msats
|
||||||
|
WHERE id = referrer_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN 0;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- add referral act on item_act
|
||||||
|
CREATE OR REPLACE FUNCTION item_act(item_id INTEGER, user_id INTEGER, act "ItemActType", act_sats INTEGER)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
user_msats BIGINT;
|
||||||
|
act_msats BIGINT;
|
||||||
|
fee_msats BIGINT;
|
||||||
|
item_act_id INTEGER;
|
||||||
|
referrer_id INTEGER;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
act_msats := act_sats * 1000;
|
||||||
|
SELECT msats, "referrerId" INTO user_msats, referrer_id FROM users WHERE id = user_id;
|
||||||
|
IF act_msats > user_msats THEN
|
||||||
|
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- deduct msats from actor
|
||||||
|
UPDATE users SET msats = msats - act_msats WHERE id = user_id;
|
||||||
|
|
||||||
|
IF act = 'VOTE' THEN
|
||||||
|
RAISE EXCEPTION 'SN_UNSUPPORTED';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF act = 'TIP' THEN
|
||||||
|
-- call to influence weightedVotes ... we need to do this before we record the acts because
|
||||||
|
-- the priors acts are taken into account
|
||||||
|
PERFORM weighted_votes_after_tip(item_id, user_id, act_sats);
|
||||||
|
|
||||||
|
-- take 10% and insert as FEE
|
||||||
|
fee_msats := CEIL(act_msats * 0.1);
|
||||||
|
act_msats := act_msats - fee_msats;
|
||||||
|
|
||||||
|
INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at)
|
||||||
|
VALUES (fee_msats, item_id, user_id, 'FEE', now_utc(), now_utc())
|
||||||
|
RETURNING id INTO item_act_id;
|
||||||
|
|
||||||
|
-- add sats to actee's balance and stacked count
|
||||||
|
UPDATE users
|
||||||
|
SET msats = msats + act_msats, "stackedMsats" = "stackedMsats" + act_msats
|
||||||
|
WHERE id = (SELECT COALESCE("fwdUserId", "userId") FROM "Item" WHERE id = item_id)
|
||||||
|
RETURNING "referrerId" INTO referrer_id;
|
||||||
|
|
||||||
|
-- leave the rest as a tip
|
||||||
|
INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at)
|
||||||
|
VALUES (act_msats, item_id, user_id, 'TIP', now_utc(), now_utc());
|
||||||
|
|
||||||
|
-- call to denormalize sats and commentSats
|
||||||
|
PERFORM sats_after_tip(item_id, user_id, act_msats + fee_msats);
|
||||||
|
ELSE -- BOOST, POLL, DONT_LIKE_THIS, STREAM
|
||||||
|
-- call to influence if DONT_LIKE_THIS weightedDownVotes
|
||||||
|
IF act = 'DONT_LIKE_THIS' THEN
|
||||||
|
-- make sure they haven't done this before
|
||||||
|
IF EXISTS (SELECT 1 FROM "ItemAct" WHERE "itemId" = item_id AND "userId" = user_id AND "ItemAct".act = 'DONT_LIKE_THIS') THEN
|
||||||
|
RAISE EXCEPTION 'SN_DUPLICATE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
PERFORM weighted_downvotes_after_act(item_id, user_id, act_sats);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at)
|
||||||
|
VALUES (act_msats, item_id, user_id, act, now_utc(), now_utc())
|
||||||
|
RETURNING id INTO item_act_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- they have a referrer and the referrer isn't the one tipping them
|
||||||
|
IF referrer_id IS NOT NULL AND user_id <> referrer_id THEN
|
||||||
|
PERFORM referral_act(referrer_id, item_act_id);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN 0;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION run_auction(item_id INTEGER) RETURNS void AS $$
|
||||||
|
DECLARE
|
||||||
|
bid_sats INTEGER;
|
||||||
|
user_msats BIGINT;
|
||||||
|
user_id INTEGER;
|
||||||
|
item_status "Status";
|
||||||
|
status_updated_at timestamp(3);
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
-- extract data we need
|
||||||
|
SELECT "maxBid", "userId", status, "statusUpdatedAt" INTO bid_sats, user_id, item_status, status_updated_at FROM "Item" WHERE id = item_id;
|
||||||
|
SELECT msats INTO user_msats FROM users WHERE id = user_id;
|
||||||
|
|
||||||
|
-- 0 bid items expire after 30 days unless updated
|
||||||
|
IF bid_sats = 0 THEN
|
||||||
|
IF item_status <> 'STOPPED' THEN
|
||||||
|
IF status_updated_at < now_utc() - INTERVAL '30 days' THEN
|
||||||
|
UPDATE "Item" SET status = 'STOPPED', "statusUpdatedAt" = now_utc() WHERE id = item_id;
|
||||||
|
ELSEIF item_status = 'NOSATS' THEN
|
||||||
|
UPDATE "Item" SET status = 'ACTIVE' WHERE id = item_id;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- check if user wallet has enough sats
|
||||||
|
IF bid_sats * 1000 > user_msats THEN
|
||||||
|
-- if not, set status = NOSATS and statusUpdatedAt to now_utc if not already set
|
||||||
|
IF item_status <> 'NOSATS' THEN
|
||||||
|
UPDATE "Item" SET status = 'NOSATS', "statusUpdatedAt" = now_utc() WHERE id = item_id;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
PERFORM item_act(item_id, user_id, 'STREAM', bid_sats);
|
||||||
|
|
||||||
|
-- update item status = ACTIVE and statusUpdatedAt = now_utc if NOSATS
|
||||||
|
IF item_status = 'NOSATS' THEN
|
||||||
|
UPDATE "Item" SET status = 'ACTIVE', "statusUpdatedAt" = now_utc() WHERE id = item_id;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- retro actively, turn all invites into referrals
|
||||||
|
UPDATE users
|
||||||
|
SET "referrerId" = subquery.inviter
|
||||||
|
FROM (
|
||||||
|
SELECT invitees.id AS invitee, inviters.id AS inviter
|
||||||
|
FROM users invitees
|
||||||
|
JOIN "Invite" ON invitees."inviteId" = "Invite".id
|
||||||
|
JOIN users inviters ON inviters.id = "Invite"."userId") subquery
|
||||||
|
WHERE id = subquery.invitee;
|
||||||
|
|
||||||
|
-- make inviters referrers too
|
||||||
|
CREATE OR REPLACE FUNCTION invite_drain(user_id INTEGER, invite_id TEXT)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
inviter_id INTEGER;
|
||||||
|
inviter_sats INTEGER;
|
||||||
|
gift INTEGER;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
-- check user was created in last hour
|
||||||
|
-- check user did not already redeem an invite
|
||||||
|
PERFORM FROM users
|
||||||
|
WHERE id = user_id AND users.created_at >= NOW() AT TIME ZONE 'UTC' - INTERVAL '1 HOUR'
|
||||||
|
AND users."inviteId" IS NULL;
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'SN_INELIGIBLE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- check that invite has not reached limit
|
||||||
|
-- check that invite is not revoked
|
||||||
|
SELECT "Invite"."userId", "Invite".gift INTO inviter_id, gift FROM "Invite"
|
||||||
|
LEFT JOIN users ON users."inviteId" = invite_id
|
||||||
|
WHERE "Invite".id = invite_id AND NOT "Invite".revoked
|
||||||
|
GROUP BY "Invite".id
|
||||||
|
HAVING COUNT(DISTINCT users.id) < "Invite".limit OR "Invite".limit IS NULL;
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- check that inviter has sufficient balance
|
||||||
|
SELECT (msats / 1000) INTO inviter_sats
|
||||||
|
FROM users WHERE id = inviter_id;
|
||||||
|
IF inviter_sats < gift THEN
|
||||||
|
RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- subtract amount from inviter
|
||||||
|
UPDATE users SET msats = msats - (1000 * gift) WHERE id = inviter_id;
|
||||||
|
-- add amount to invitee
|
||||||
|
UPDATE users SET msats = msats + (1000 * gift), "inviteId" = invite_id, "referrerId" = inviter_id WHERE id = user_id;
|
||||||
|
|
||||||
|
RETURN 0;
|
||||||
|
END;
|
||||||
|
$$;
|
|
@ -47,6 +47,11 @@ model User {
|
||||||
upvotePopover Boolean @default(false)
|
upvotePopover Boolean @default(false)
|
||||||
tipPopover Boolean @default(false)
|
tipPopover Boolean @default(false)
|
||||||
|
|
||||||
|
// referrals
|
||||||
|
referrer User? @relation("referrals", fields: [referrerId], references: [id])
|
||||||
|
referrerId Int?
|
||||||
|
referrees User[] @relation("referrals")
|
||||||
|
|
||||||
// tip settings
|
// tip settings
|
||||||
tipDefault Int @default(10)
|
tipDefault Int @default(10)
|
||||||
turboTipping Boolean @default(false)
|
turboTipping Boolean @default(false)
|
||||||
|
@ -68,10 +73,11 @@ model User {
|
||||||
wildWestMode Boolean @default(false)
|
wildWestMode Boolean @default(false)
|
||||||
greeterMode Boolean @default(false)
|
greeterMode Boolean @default(false)
|
||||||
|
|
||||||
Earn Earn[]
|
Earn Earn[]
|
||||||
Upload Upload[] @relation(name: "Uploads")
|
Upload Upload[] @relation(name: "Uploads")
|
||||||
PollVote PollVote[]
|
PollVote PollVote[]
|
||||||
Donation Donation[]
|
Donation Donation[]
|
||||||
|
ReferralAct ReferralAct[]
|
||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([inviteId])
|
@@index([inviteId])
|
||||||
|
@ -318,6 +324,17 @@ model Pin {
|
||||||
Item Item[]
|
Item Item[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ReferralAct {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
||||||
|
referrerId Int
|
||||||
|
referrer User @relation(fields: [referrerId], references: [id])
|
||||||
|
itemActId Int
|
||||||
|
itemAct ItemAct @relation(fields: [itemActId], references: [id])
|
||||||
|
msats BigInt
|
||||||
|
}
|
||||||
|
|
||||||
enum ItemActType {
|
enum ItemActType {
|
||||||
VOTE
|
VOTE
|
||||||
BOOST
|
BOOST
|
||||||
|
@ -329,15 +346,16 @@ enum ItemActType {
|
||||||
}
|
}
|
||||||
|
|
||||||
model ItemAct {
|
model ItemAct {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||||
msats BigInt
|
msats BigInt
|
||||||
act ItemActType
|
act ItemActType
|
||||||
item Item @relation(fields: [itemId], references: [id])
|
item Item @relation(fields: [itemId], references: [id])
|
||||||
itemId Int
|
itemId Int
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
userId Int
|
userId Int
|
||||||
|
ReferralAct ReferralAct[]
|
||||||
|
|
||||||
@@index([itemId])
|
@@index([itemId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M13.576 17.271l-5.11-2.787a3.5 3.5 0 1 1 0-4.968l5.11-2.787a3.5 3.5 0 1 1 .958 1.755l-5.11 2.787a3.514 3.514 0 0 1 0 1.458l5.11 2.787a3.5 3.5 0 1 1-.958 1.755z"/></svg>
|
After Width: | Height: | Size: 297 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M13 14h-2a8.999 8.999 0 0 0-7.968 4.81A10.136 10.136 0 0 1 3 18C3 12.477 7.477 8 13 8V3l10 8-10 8v-5z"/></svg>
|
After Width: | Height: | Size: 239 B |
|
@ -11,9 +11,10 @@ function earn ({ models }) {
|
||||||
|
|
||||||
// compute how much sn earned today
|
// compute how much sn earned today
|
||||||
let [{ sum }] = await models.$queryRaw`
|
let [{ sum }] = await models.$queryRaw`
|
||||||
SELECT coalesce(sum("ItemAct".msats), 0) as sum
|
SELECT coalesce(sum("ItemAct".msats - coalesce("ReferralAct".msats, 0)), 0) as sum
|
||||||
FROM "ItemAct"
|
FROM "ItemAct"
|
||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
JOIN "Item" ON "ItemAct"."itemId" = "Item".id
|
||||||
|
LEFT JOIN "ReferralAct" ON "ItemAct".id = "ReferralAct"."itemActId"
|
||||||
WHERE "ItemAct".act <> 'TIP'
|
WHERE "ItemAct".act <> 'TIP'
|
||||||
AND "ItemAct".created_at > now_utc() - INTERVAL '1 day'`
|
AND "ItemAct".created_at > now_utc() - INTERVAL '1 day'`
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue