Compare commits
No commits in common. "dd4806c1a3d1a3410d0c0d3d4da4a95eba9eb528" and "6cf16d3da7790e4d36fe42295242506e9a3cc9fd" have entirely different histories.
dd4806c1a3
...
6cf16d3da7
@ -200,16 +200,15 @@ export async function onPaid ({ invoice, id }, context) {
|
|||||||
// denormalize ncomments, lastCommentAt, and "weightedComments" for ancestors, and insert into reply table
|
// denormalize ncomments, lastCommentAt, and "weightedComments" for ancestors, and insert into reply table
|
||||||
await tx.$executeRaw`
|
await tx.$executeRaw`
|
||||||
WITH comment AS (
|
WITH comment AS (
|
||||||
SELECT "Item".*, users.trust
|
SELECT *
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
JOIN users ON "Item"."userId" = users.id
|
WHERE id = ${item.id}::INTEGER
|
||||||
WHERE "Item".id = ${item.id}::INTEGER
|
|
||||||
), ancestors AS (
|
), ancestors AS (
|
||||||
UPDATE "Item"
|
UPDATE "Item"
|
||||||
SET ncomments = "Item".ncomments + 1,
|
SET ncomments = "Item".ncomments + 1,
|
||||||
"lastCommentAt" = now(),
|
"lastCommentAt" = now(),
|
||||||
"weightedComments" = "Item"."weightedComments" +
|
"weightedComments" = "Item"."weightedComments" +
|
||||||
CASE WHEN comment."userId" = "Item"."userId" THEN 0 ELSE comment.trust END
|
CASE WHEN comment."userId" = "Item"."userId" THEN 0 ELSE ${item.user.trust}::FLOAT END
|
||||||
FROM comment
|
FROM comment
|
||||||
WHERE "Item".path @> comment.path AND "Item".id <> comment.id
|
WHERE "Item".path @> comment.path AND "Item".id <> comment.id
|
||||||
RETURNING "Item".*
|
RETURNING "Item".*
|
||||||
|
@ -1,5 +1,26 @@
|
|||||||
import { timeUnitForRange, whenRange } from '@/lib/time'
|
import { timeUnitForRange, whenRange } from '@/lib/time'
|
||||||
|
|
||||||
|
export function withClause (range) {
|
||||||
|
const unit = timeUnitForRange(range)
|
||||||
|
|
||||||
|
return `
|
||||||
|
WITH range_values AS (
|
||||||
|
SELECT date_trunc('${unit}', $1) as minval,
|
||||||
|
date_trunc('${unit}', $2) as maxval
|
||||||
|
),
|
||||||
|
times AS (
|
||||||
|
SELECT generate_series(minval, maxval, interval '1 ${unit}') as time
|
||||||
|
FROM range_values
|
||||||
|
)
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function intervalClause (range, table) {
|
||||||
|
const unit = timeUnitForRange(range)
|
||||||
|
|
||||||
|
return `date_trunc('${unit}', "${table}".created_at) >= date_trunc('${unit}', $1) AND date_trunc('${unit}', "${table}".created_at) <= date_trunc('${unit}', $2) `
|
||||||
|
}
|
||||||
|
|
||||||
export function viewIntervalClause (range, view) {
|
export function viewIntervalClause (range, view) {
|
||||||
const unit = timeUnitForRange(range)
|
const unit = timeUnitForRange(range)
|
||||||
return `"${view}".t >= date_trunc('${unit}', timezone('America/Chicago', $1)) AND date_trunc('${unit}', "${view}".t) <= date_trunc('${unit}', timezone('America/Chicago', $2)) `
|
return `"${view}".t >= date_trunc('${unit}', timezone('America/Chicago', $1)) AND date_trunc('${unit}', "${view}".t) <= date_trunc('${unit}', timezone('America/Chicago', $2)) `
|
||||||
@ -21,8 +42,8 @@ export function viewGroup (range, view) {
|
|||||||
${view}(
|
${view}(
|
||||||
date_trunc('hour', timezone('America/Chicago', now())),
|
date_trunc('hour', timezone('America/Chicago', now())),
|
||||||
date_trunc('hour', timezone('America/Chicago', now())), '1 hour'::INTERVAL, 'hour')
|
date_trunc('hour', timezone('America/Chicago', now())), '1 hour'::INTERVAL, 'hour')
|
||||||
WHERE "${view}".t >= date_trunc('hour', timezone('America/Chicago', $1))
|
WHERE "${view}".t >= date_trunc('${unit}', timezone('America/Chicago', $1))
|
||||||
AND "${view}".t <= date_trunc('hour', timezone('America/Chicago', $2)))
|
AND "${view}".t <= date_trunc('${unit}', timezone('America/Chicago', $2)))
|
||||||
) u`
|
) u`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1155,21 +1155,7 @@ export default {
|
|||||||
if (item.root) {
|
if (item.root) {
|
||||||
return item.root
|
return item.root
|
||||||
}
|
}
|
||||||
|
return await getItem(item, { id: item.rootId }, { me, models })
|
||||||
// we can't use getItem because activeOrMine will prevent root from being fetched
|
|
||||||
const [root] = await itemQueryWithMeta({
|
|
||||||
me,
|
|
||||||
models,
|
|
||||||
query: `
|
|
||||||
${SELECT}
|
|
||||||
FROM "Item"
|
|
||||||
${whereClause(
|
|
||||||
'"Item".id = $1',
|
|
||||||
`("Item"."invoiceActionState" IS NULL OR "Item"."invoiceActionState" = 'PAID'${me ? ` OR "Item"."userId" = ${me.id}` : ''})`
|
|
||||||
)}`
|
|
||||||
}, Number(item.rootId))
|
|
||||||
|
|
||||||
return root
|
|
||||||
},
|
},
|
||||||
invoice: async (item, args, { models }) => {
|
invoice: async (item, args, { models }) => {
|
||||||
if (item.invoiceId) {
|
if (item.invoiceId) {
|
||||||
|
@ -284,7 +284,6 @@ export default {
|
|||||||
FROM "Earn"
|
FROM "Earn"
|
||||||
WHERE "userId" = $1
|
WHERE "userId" = $1
|
||||||
AND created_at < $2
|
AND created_at < $2
|
||||||
AND (type IS NULL OR type NOT IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL'))
|
|
||||||
GROUP BY "userId", created_at
|
GROUP BY "userId", created_at
|
||||||
ORDER BY "sortTime" DESC
|
ORDER BY "sortTime" DESC
|
||||||
LIMIT ${LIMIT})`
|
LIMIT ${LIMIT})`
|
||||||
@ -300,17 +299,6 @@ export default {
|
|||||||
ORDER BY "sortTime" DESC
|
ORDER BY "sortTime" DESC
|
||||||
LIMIT ${LIMIT})`
|
LIMIT ${LIMIT})`
|
||||||
)
|
)
|
||||||
queries.push(
|
|
||||||
`(SELECT min(id)::text, created_at AS "sortTime", FLOOR(sum(msats) / 1000) as "earnedSats",
|
|
||||||
'ReferralReward' AS type
|
|
||||||
FROM "Earn"
|
|
||||||
WHERE "userId" = $1
|
|
||||||
AND created_at < $2
|
|
||||||
AND type IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL')
|
|
||||||
GROUP BY "userId", created_at
|
|
||||||
ORDER BY "sortTime" DESC
|
|
||||||
LIMIT ${LIMIT})`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meFull.noteCowboyHat) {
|
if (meFull.noteCowboyHat) {
|
||||||
@ -499,22 +487,6 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ReferralReward: {
|
|
||||||
sources: async (n, args, { me, models }) => {
|
|
||||||
const [sources] = await models.$queryRawUnsafe(`
|
|
||||||
SELECT
|
|
||||||
COALESCE(FLOOR(sum(msats) FILTER(WHERE type = 'FOREVER_REFERRAL') / 1000), 0) AS forever,
|
|
||||||
COALESCE(FLOOR(sum(msats) FILTER(WHERE type = 'ONE_DAY_REFERRAL') / 1000), 0) AS "oneDay"
|
|
||||||
FROM "Earn"
|
|
||||||
WHERE "userId" = $1 AND created_at = $2
|
|
||||||
`, Number(me.id), new Date(n.sortTime))
|
|
||||||
if (sources.forever + sources.oneDay > 0) {
|
|
||||||
return sources
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Mention: {
|
Mention: {
|
||||||
mention: async (n, args, { models }) => true,
|
mention: async (n, args, { models }) => true,
|
||||||
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
|
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { GraphQLError } from 'graphql'
|
import { GraphQLError } from 'graphql'
|
||||||
|
import { withClause, intervalClause } from './growth'
|
||||||
import { timeUnitForRange, whenRange } from '@/lib/time'
|
import { timeUnitForRange, whenRange } from '@/lib/time'
|
||||||
import { viewGroup } from './growth'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
@ -11,18 +11,46 @@ export default {
|
|||||||
|
|
||||||
const range = whenRange(when, from, to)
|
const range = whenRange(when, from, to)
|
||||||
|
|
||||||
return await models.$queryRawUnsafe(`
|
const [{ totalSats }] = await models.$queryRawUnsafe(`
|
||||||
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time,
|
SELECT COALESCE(FLOOR(sum(msats) / 1000), 0) as "totalSats"
|
||||||
json_build_array(
|
FROM "ReferralAct"
|
||||||
json_build_object('name', 'referrals', 'value', COALESCE(SUM(referrals), 0)),
|
WHERE ${intervalClause(range, 'ReferralAct')}
|
||||||
json_build_object('name', 'one day referrals', 'value', COALESCE(SUM(one_day_referrals), 0)),
|
AND "ReferralAct"."referrerId" = $3
|
||||||
json_build_object('name', 'referral sats', 'value', FLOOR(COALESCE(SUM(msats_referrals), 0) / 1000.0)),
|
`, ...range, Number(me.id))
|
||||||
json_build_object('name', 'one day referral sats', 'value', FLOOR(COALESCE(SUM(msats_one_day_referrals), 0) / 1000.0))
|
|
||||||
|
const [{ totalReferrals }] = await models.$queryRawUnsafe(`
|
||||||
|
SELECT count(*)::INTEGER as "totalReferrals"
|
||||||
|
FROM users
|
||||||
|
WHERE ${intervalClause(range, 'users')}
|
||||||
|
AND "referrerId" = $3
|
||||||
|
`, ...range, Number(me.id))
|
||||||
|
|
||||||
|
const stats = await models.$queryRawUnsafe(
|
||||||
|
`${withClause(range)}
|
||||||
|
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
|
) AS data
|
||||||
FROM ${viewGroup(range, 'user_stats')}
|
FROM times
|
||||||
WHERE id = ${me.id}
|
LEFT JOIN
|
||||||
GROUP BY time
|
((SELECT "ReferralAct".created_at, "ReferralAct".msats / 1000.0 as msats, "ItemAct".act::text as act
|
||||||
ORDER BY time ASC`, ...range)
|
FROM "ReferralAct"
|
||||||
|
JOIN "ItemAct" ON "ItemAct".id = "ReferralAct"."itemActId"
|
||||||
|
WHERE ${intervalClause(range, 'ReferralAct')}
|
||||||
|
AND "ReferralAct"."referrerId" = $3)
|
||||||
|
UNION ALL
|
||||||
|
(SELECT created_at, 0.0 as sats, 'REFERREE' as act
|
||||||
|
FROM users
|
||||||
|
WHERE ${intervalClause(range, 'users')}
|
||||||
|
AND "referrerId" = $3)) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at)
|
||||||
|
GROUP BY time
|
||||||
|
ORDER BY time ASC`, ...range, Number(me.id))
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalSats,
|
||||||
|
totalReferrals,
|
||||||
|
stats
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -579,8 +579,7 @@ export default {
|
|||||||
json_build_object('name', 'comments', 'value', COALESCE(SUM(comments), 0)),
|
json_build_object('name', 'comments', 'value', COALESCE(SUM(comments), 0)),
|
||||||
json_build_object('name', 'posts', 'value', COALESCE(SUM(posts), 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', 'territories', 'value', COALESCE(SUM(territories), 0)),
|
||||||
json_build_object('name', 'referrals', 'value', COALESCE(SUM(referrals), 0)),
|
json_build_object('name', 'referrals', 'value', COALESCE(SUM(referrals), 0))
|
||||||
json_build_object('name', 'one day referrals', 'value', COALESCE(SUM(one_day_referrals), 0))
|
|
||||||
) AS data
|
) AS data
|
||||||
FROM ${viewGroup(range, 'user_stats')}
|
FROM ${viewGroup(range, 'user_stats')}
|
||||||
WHERE id = ${me.id}
|
WHERE id = ${me.id}
|
||||||
@ -595,7 +594,6 @@ export default {
|
|||||||
json_build_object('name', 'zaps', 'value', ROUND(COALESCE(SUM(msats_tipped), 0) / 1000)),
|
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', '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', 'referrals', 'value', ROUND( COALESCE(SUM(msats_referrals), 0) / 1000)),
|
||||||
json_build_object('name', 'one day referrals', 'value', ROUND( COALESCE(SUM(msats_one_day_referrals), 0) / 1000)),
|
|
||||||
json_build_object('name', 'territories', 'value', ROUND(COALESCE(SUM(msats_revenue), 0) / 1000))
|
json_build_object('name', 'territories', 'value', ROUND(COALESCE(SUM(msats_revenue), 0) / 1000))
|
||||||
) AS data
|
) AS data
|
||||||
FROM ${viewGroup(range, 'user_stats')}
|
FROM ${viewGroup(range, 'user_stats')}
|
||||||
@ -609,7 +607,6 @@ export default {
|
|||||||
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time,
|
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time,
|
||||||
json_build_array(
|
json_build_array(
|
||||||
json_build_object('name', 'fees', 'value', FLOOR(COALESCE(SUM(msats_fees), 0) / 1000)),
|
json_build_object('name', 'fees', 'value', FLOOR(COALESCE(SUM(msats_fees), 0) / 1000)),
|
||||||
json_build_object('name', 'zapping', 'value', FLOOR(COALESCE(SUM(msats_zaps), 0) / 1000)),
|
|
||||||
json_build_object('name', 'donations', 'value', FLOOR(COALESCE(SUM(msats_donated), 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))
|
json_build_object('name', 'territories', 'value', FLOOR(COALESCE(SUM(msats_billing), 0) / 1000))
|
||||||
) AS data
|
) AS data
|
||||||
|
@ -72,16 +72,7 @@ function oneDayReferral (request, { me }) {
|
|||||||
typeId: String(item.id)
|
typeId: String(item.id)
|
||||||
})
|
})
|
||||||
} else if (referrer.startsWith('profile-')) {
|
} else if (referrer.startsWith('profile-')) {
|
||||||
const name = referrer.slice(8)
|
prismaPromise = models.user.findUnique({ where: { name: referrer.slice(8) } })
|
||||||
// exclude all pages that are not user profiles
|
|
||||||
if (['api', 'auth', 'day', 'invites', 'invoices', 'referrals', 'rewards',
|
|
||||||
'satistics', 'settings', 'stackers', 'wallet', 'withdrawals', '404', '500',
|
|
||||||
'email', 'live', 'login', 'notifications', 'offline', 'search', 'share',
|
|
||||||
'signup', 'territory', 'recent', 'top', 'edit', 'post', 'rss', 'saloon',
|
|
||||||
'faq', 'story', 'privacy', 'copyright', 'tos', 'changes', 'guide', 'daily',
|
|
||||||
'anon', 'ad'].includes(name)) continue
|
|
||||||
|
|
||||||
prismaPromise = models.user.findUnique({ where: { name } })
|
|
||||||
getData = user => ({
|
getData = user => ({
|
||||||
referrerId: user.id,
|
referrerId: user.id,
|
||||||
refereeId: parseInt(me.id),
|
refereeId: parseInt(me.id),
|
||||||
|
@ -89,19 +89,6 @@ export default gql`
|
|||||||
sources: EarnSources
|
sources: EarnSources
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReferralSources {
|
|
||||||
id: ID!
|
|
||||||
forever: Int!
|
|
||||||
oneDay: Int!
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReferralReward {
|
|
||||||
id: ID!
|
|
||||||
earnedSats: Int!
|
|
||||||
sortTime: Date!
|
|
||||||
sources: ReferralSources
|
|
||||||
}
|
|
||||||
|
|
||||||
type Revenue {
|
type Revenue {
|
||||||
id: ID!
|
id: ID!
|
||||||
earnedSats: Int!
|
earnedSats: Int!
|
||||||
@ -156,7 +143,6 @@ export default gql`
|
|||||||
| Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral
|
| Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral
|
||||||
| Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus
|
| Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus
|
||||||
| TerritoryPost | TerritoryTransfer | Reminder | ItemMention | Invoicification
|
| TerritoryPost | TerritoryTransfer | Reminder | ItemMention | Invoicification
|
||||||
| ReferralReward
|
|
||||||
|
|
||||||
type Notifications {
|
type Notifications {
|
||||||
lastChecked: Date
|
lastChecked: Date
|
||||||
|
@ -2,6 +2,12 @@ import { gql } from 'graphql-tag'
|
|||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
extend type Query {
|
extend type Query {
|
||||||
referrals(when: String, from: String, to: String): [TimeData!]!
|
referrals(when: String, from: String, to: String): Referrals!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Referrals {
|
||||||
|
totalSats: Int!
|
||||||
|
totalReferrals: Int!
|
||||||
|
stats: [TimeData!]!
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -113,5 +113,3 @@ felipebueno,issue,#1231,#1230,good-first-issue,,,,2k,felipebueno@getalby.com,202
|
|||||||
tsmith123,pr,#1223,#107,medium,,2,10k bonus for our slowness,210k,stickymarch60@walletofsatoshi.com,2024-06-22
|
tsmith123,pr,#1223,#107,medium,,2,10k bonus for our slowness,210k,stickymarch60@walletofsatoshi.com,2024-06-22
|
||||||
cointastical,issue,#1223,#107,medium,,2,,20k,cointastical@stacker.news,2024-06-22
|
cointastical,issue,#1223,#107,medium,,2,,20k,cointastical@stacker.news,2024-06-22
|
||||||
kravhen,pr,#1215,#253,medium,,2,upgraded to medium,200k,nichro@getalby.com,2024-06-28
|
kravhen,pr,#1215,#253,medium,,2,upgraded to medium,200k,nichro@getalby.com,2024-06-28
|
||||||
dillon-co,pr,#1140,#633,hard,,,requested advance,500k,bolt11,2024-07-02
|
|
||||||
takitakitanana,issue,,#1257,good-first-issue,,,,2k,takitakitanana@stacker.news,2024-07-11
|
|
||||||
|
|
@ -15,19 +15,12 @@ export default function ActionTooltip ({ children, notForm, disable, overlayText
|
|||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
placement={placement || 'bottom'}
|
placement={placement || 'bottom'}
|
||||||
overlay={
|
overlay={
|
||||||
<Tooltip style={{ position: 'fixed' }}>
|
<Tooltip>
|
||||||
{overlayText}
|
{overlayText}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
trigger={['hover', 'focus']}
|
trigger={['hover', 'focus']}
|
||||||
show={formik?.isSubmitting ? false : undefined}
|
show={formik?.isSubmitting ? false : undefined}
|
||||||
popperConfig={{
|
|
||||||
modifiers: {
|
|
||||||
preventOverflow: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{children}
|
{children}
|
||||||
|
@ -156,7 +156,7 @@ export function WhenComposedChart ({
|
|||||||
data,
|
data,
|
||||||
lineNames = [], lineAxis = 'left',
|
lineNames = [], lineAxis = 'left',
|
||||||
areaNames = [], areaAxis = 'left',
|
areaNames = [], areaAxis = 'left',
|
||||||
barNames = [], barAxis = 'left', barStackId
|
barNames = [], barAxis = 'left'
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
@ -189,7 +189,7 @@ export function WhenComposedChart ({
|
|||||||
<Tooltip labelFormatter={labelFormatter(when, from, to)} contentStyle={{ color: 'var(--bs-body-color)', backgroundColor: 'var(--bs-body-bg)' }} />
|
<Tooltip labelFormatter={labelFormatter(when, from, to)} contentStyle={{ color: 'var(--bs-body-color)', backgroundColor: 'var(--bs-body-bg)' }} />
|
||||||
<Legend />
|
<Legend />
|
||||||
{barNames?.map((v, i) =>
|
{barNames?.map((v, i) =>
|
||||||
<Bar yAxisId={barAxis} key={v} stackId={barStackId} type='monotone' dataKey={v} name={v} stroke={getColor(i)} fill={getColor(i)} />)}
|
<Bar yAxisId={barAxis} key={v} type='monotone' dataKey={v} name={v} stroke={getColor(i)} fill={getColor(i)} />)}
|
||||||
{areaNames?.map((v, i) =>
|
{areaNames?.map((v, i) =>
|
||||||
<Area yAxisId={areaAxis} key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={getColor(barNames.length + i)} fill={getColor(barNames.length + i)} />)}
|
<Area yAxisId={areaAxis} key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={getColor(barNames.length + i)} fill={getColor(barNames.length + i)} />)}
|
||||||
{lineNames?.map((v, i) =>
|
{lineNames?.map((v, i) =>
|
||||||
|
@ -77,7 +77,7 @@ export function CommentFlat ({ item, rank, siblingComments, ...props }) {
|
|||||||
</div>)
|
</div>)
|
||||||
: <div />}
|
: <div />}
|
||||||
<LinkToContext
|
<LinkToContext
|
||||||
className='py-2'
|
className={siblingComments ? 'py-3' : 'py-2'}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
router.push(href, as)
|
router.push(href, as)
|
||||||
}}
|
}}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
.item {
|
.item {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
padding-top: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upvote {
|
.upvote {
|
||||||
margin-top: 9px;
|
margin-top: 9px;
|
||||||
padding-right: 0.2rem;
|
margin-left: .25rem;
|
||||||
|
margin-right: 0rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pin {
|
.pin {
|
||||||
@ -64,7 +65,7 @@
|
|||||||
|
|
||||||
.children {
|
.children {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-left: 27px;
|
margin-left: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comments {
|
.comments {
|
||||||
@ -108,7 +109,7 @@
|
|||||||
.comment {
|
.comment {
|
||||||
border-radius: .4rem;
|
border-radius: .4rem;
|
||||||
padding-top: .5rem;
|
padding-top: .5rem;
|
||||||
padding-left: .7rem;
|
padding-left: .2rem;
|
||||||
background-color: var(--theme-commentBg);
|
background-color: var(--theme-commentBg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,11 +129,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.comment:not(:first-of-type) {
|
.comment:not(:first-of-type) {
|
||||||
padding-top: 0;
|
padding-top: .25rem;
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment:has(.comment) + .comment{
|
|
||||||
padding-top: .5rem;
|
|
||||||
}
|
|
@ -26,20 +26,11 @@ export default function HoverablePopover ({ id, trigger, body, onShow }) {
|
|||||||
show={showOverlay}
|
show={showOverlay}
|
||||||
placement='bottom'
|
placement='bottom'
|
||||||
onHide={handleMouseLeave}
|
onHide={handleMouseLeave}
|
||||||
popperConfig={{
|
|
||||||
modifiers: {
|
|
||||||
preventOverflow: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
overlay={
|
overlay={
|
||||||
<Popover
|
<Popover
|
||||||
onPointerEnter={handleMouseEnter}
|
onPointerEnter={handleMouseEnter}
|
||||||
onPointerLeave={handleMouseLeave}
|
onPointerLeave={handleMouseLeave}
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
className={styles.HoverablePopover}
|
className={styles.HoverablePopover}
|
||||||
style={{ position: 'fixed' }}
|
|
||||||
>
|
>
|
||||||
<Popover.Body className={styles.HoverablePopover}>
|
<Popover.Body className={styles.HoverablePopover}>
|
||||||
{body}
|
{body}
|
||||||
|
@ -46,7 +46,7 @@ export function SearchTitle ({ title }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Item ({
|
export default function Item ({
|
||||||
item, rank, belowTitle, right, full, children, itemClassName,
|
item, rank, belowTitle, right, full, children, siblingComments,
|
||||||
onQuoteReply, pinnable
|
onQuoteReply, pinnable
|
||||||
}) {
|
}) {
|
||||||
const titleRef = useRef()
|
const titleRef = useRef()
|
||||||
@ -62,7 +62,7 @@ export default function Item ({
|
|||||||
{rank}
|
{rank}
|
||||||
</div>)
|
</div>)
|
||||||
: <div />}
|
: <div />}
|
||||||
<div className={classNames(styles.item, itemClassName)}>
|
<div className={`${styles.item} ${siblingComments ? 'pt-3' : ''}`}>
|
||||||
{item.position && (pinnable || !item.subName)
|
{item.position && (pinnable || !item.subName)
|
||||||
? <Pin width={24} height={24} className={styles.pin} />
|
? <Pin width={24} height={24} className={styles.pin} />
|
||||||
: item.meDontLikeSats > item.meSats
|
: item.meDontLikeSats > item.meSats
|
||||||
|
@ -118,7 +118,7 @@ a.link:visited {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-top: .5rem;
|
padding-bottom: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item .companyImage {
|
.item .companyImage {
|
||||||
@ -169,8 +169,7 @@ a.link:visited {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.children {
|
.children {
|
||||||
margin-left: 27px;
|
margin-left: 28px;
|
||||||
padding-top: .5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank {
|
.rank {
|
||||||
|
@ -51,7 +51,7 @@ export default function Items ({ ssrData, variables = {}, query, destructureData
|
|||||||
<>
|
<>
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{itemsWithPins.filter(filter).map((item, i) => (
|
{itemsWithPins.filter(filter).map((item, i) => (
|
||||||
<ListItem key={item.id} item={item} rank={rank && i + 1} itemClassName={variables.includeComments ? 'py-2' : ''} pinnable={isHome ? false : pins?.length > 0} />
|
<ListItem key={item.id} item={item} rank={rank && i + 1} siblingComments={variables.includeComments} pinnable={isHome ? false : pins?.length > 0} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Foooter
|
<Foooter
|
||||||
|
@ -12,10 +12,6 @@
|
|||||||
|
|
||||||
.linkBoxParent {
|
.linkBoxParent {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-left: -0.5rem;
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
margin-right: -0.5rem;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.linkBoxParent > * {
|
.linkBoxParent > * {
|
||||||
|
@ -33,7 +33,7 @@ export default function MoreFooter ({ cursor, count, fetchMore, Skeleton, invisi
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={`d-flex justify-content-center mt-4 mb-1 ${invisible ? 'invisible' : ''}`}><Footer /></div>
|
return <div className={`d-flex justify-content-center mt-3 mb-1 ${invisible ? 'invisible' : ''}`}><Footer /></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NavigateFooter ({ cursor, count, fetchMore, href, text, invisible, noMoreText = 'NO MORE' }) {
|
export function NavigateFooter ({ cursor, count, fetchMore, href, text, invisible, noMoreText = 'NO MORE' }) {
|
||||||
|
@ -6,7 +6,7 @@ export default function SecondBar (props) {
|
|||||||
const { prefix, topNavKey, sub } = props
|
const { prefix, topNavKey, sub } = props
|
||||||
if (!hasNavSelect(props)) return null
|
if (!hasNavSelect(props)) return null
|
||||||
return (
|
return (
|
||||||
<Navbar className='pt-0 pb-2'>
|
<Navbar className='pt-0 pb-3'>
|
||||||
<Nav
|
<Nav
|
||||||
className={styles.navbarNav}
|
className={styles.navbarNav}
|
||||||
activeKey={topNavKey}
|
activeKey={topNavKey}
|
||||||
|
@ -10,7 +10,6 @@ import { dayMonthYear, timeSince } from '@/lib/time'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Check from '@/svgs/check-double-line.svg'
|
import Check from '@/svgs/check-double-line.svg'
|
||||||
import HandCoin from '@/svgs/hand-coin-fill.svg'
|
import HandCoin from '@/svgs/hand-coin-fill.svg'
|
||||||
import UserAdd from '@/svgs/user-add-fill.svg'
|
|
||||||
import { LOST_BLURBS, FOUND_BLURBS, UNKNOWN_LINK_REL } from '@/lib/constants'
|
import { LOST_BLURBS, FOUND_BLURBS, UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||||
import CowboyHatIcon from '@/svgs/cowboy.svg'
|
import CowboyHatIcon from '@/svgs/cowboy.svg'
|
||||||
import BaldIcon from '@/svgs/bald.svg'
|
import BaldIcon from '@/svgs/bald.svg'
|
||||||
@ -43,7 +42,7 @@ function Notification ({ n, fresh }) {
|
|||||||
const type = n.__typename
|
const type = n.__typename
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationLayout nid={nid(n)} type={type} {...defaultOnClick(n)} fresh={fresh}>
|
<NotificationLayout nid={nid(n)} {...defaultOnClick(n)} fresh={fresh}>
|
||||||
{
|
{
|
||||||
(type === 'Earn' && <EarnNotification n={n} />) ||
|
(type === 'Earn' && <EarnNotification n={n} />) ||
|
||||||
(type === 'Revenue' && <RevenueNotification n={n} />) ||
|
(type === 'Revenue' && <RevenueNotification n={n} />) ||
|
||||||
@ -63,19 +62,18 @@ function Notification ({ n, fresh }) {
|
|||||||
(type === 'TerritoryPost' && <TerritoryPost n={n} />) ||
|
(type === 'TerritoryPost' && <TerritoryPost n={n} />) ||
|
||||||
(type === 'TerritoryTransfer' && <TerritoryTransfer n={n} />) ||
|
(type === 'TerritoryTransfer' && <TerritoryTransfer n={n} />) ||
|
||||||
(type === 'Reminder' && <Reminder n={n} />) ||
|
(type === 'Reminder' && <Reminder n={n} />) ||
|
||||||
(type === 'Invoicification' && <Invoicification n={n} />) ||
|
(type === 'Invoicification' && <Invoicification n={n} />)
|
||||||
(type === 'ReferralReward' && <ReferralReward n={n} />)
|
|
||||||
}
|
}
|
||||||
</NotificationLayout>
|
</NotificationLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotificationLayout ({ children, type, nid, href, as, fresh }) {
|
function NotificationLayout ({ children, nid, href, as, fresh }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
if (!href) return <div className={`py-2 ${fresh ? styles.fresh : ''}`}>{children}</div>
|
if (!href) return <div className={fresh ? styles.fresh : ''}>{children}</div>
|
||||||
return (
|
return (
|
||||||
<LinkToContext
|
<LinkToContext
|
||||||
className={`py-2 ${type === 'Reply' ? styles.reply : ''} ${fresh ? styles.fresh : ''} ${router?.query?.nid === nid ? 'outline-it' : ''}`}
|
className={`${fresh ? styles.fresh : ''} ${router?.query?.nid === nid ? 'outline-it' : ''}`}
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
nid && await router.replace({
|
nid && await router.replace({
|
||||||
@ -94,27 +92,6 @@ function NotificationLayout ({ children, type, nid, href, as, fresh }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NoteHeader ({ color, children, big }) {
|
|
||||||
return (
|
|
||||||
<div className={`${styles.noteHeader} text-${color} ${big ? '' : 'small'} pb-2`}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NoteItem ({ item }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{item.title
|
|
||||||
? <Item item={item} itemClassName='pt-0' />
|
|
||||||
: (
|
|
||||||
<RootProvider root={item.root}>
|
|
||||||
<Comment item={item} noReply includeParent clickToContext />
|
|
||||||
</RootProvider>)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultOnClick = n => {
|
const defaultOnClick = n => {
|
||||||
const type = n.__typename
|
const type = n.__typename
|
||||||
if (type === 'Earn') {
|
if (type === 'Earn') {
|
||||||
@ -155,7 +132,6 @@ const defaultOnClick = n => {
|
|||||||
if (type === 'Invoicification') return itemLink(n.invoice.item)
|
if (type === 'Invoicification') return itemLink(n.invoice.item)
|
||||||
if (type === 'WithdrawlPaid') return { href: `/withdrawals/${n.id}` }
|
if (type === 'WithdrawlPaid') return { href: `/withdrawals/${n.id}` }
|
||||||
if (type === 'Referral') return { href: '/referrals/month' }
|
if (type === 'Referral') return { href: '/referrals/month' }
|
||||||
if (type === 'ReferralReward') return { href: '/referrals/month' }
|
|
||||||
if (type === 'Streak') return {}
|
if (type === 'Streak') return {}
|
||||||
if (type === 'TerritoryTransfer') return { href: `/~${n.sub.name}` }
|
if (type === 'TerritoryTransfer') return { href: `/~${n.sub.name}` }
|
||||||
|
|
||||||
@ -180,7 +156,7 @@ function Streak ({ n }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='d-flex'>
|
<div className='d-flex ms-2 py-1'>
|
||||||
<div style={{ fontSize: '2rem' }}>{n.days ? <BaldIcon className='fill-grey' height={40} width={40} /> : <CowboyHatIcon className='fill-grey' height={40} width={40} />}</div>
|
<div style={{ fontSize: '2rem' }}>{n.days ? <BaldIcon className='fill-grey' height={40} width={40} /> : <CowboyHatIcon className='fill-grey' height={40} width={40} />}</div>
|
||||||
<div className='ms-1 p-1'>
|
<div className='ms-1 p-1'>
|
||||||
<span className='fw-bold'>you {n.days ? 'lost your' : 'found a'} cowboy hat</span>
|
<span className='fw-bold'>you {n.days ? 'lost your' : 'found a'} cowboy hat</span>
|
||||||
@ -194,12 +170,12 @@ function EarnNotification ({ n }) {
|
|||||||
const time = n.minSortTime === n.sortTime ? dayMonthYear(new Date(n.minSortTime)) : `${dayMonthYear(new Date(n.minSortTime))} to ${dayMonthYear(new Date(n.sortTime))}`
|
const time = n.minSortTime === n.sortTime ? dayMonthYear(new Date(n.minSortTime)) : `${dayMonthYear(new Date(n.minSortTime))} to ${dayMonthYear(new Date(n.sortTime))}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='d-flex'>
|
<div className='d-flex ms-2 py-1'>
|
||||||
<HandCoin className='align-self-center fill-boost mx-1' width={24} height={24} style={{ flex: '0 0 24px', transform: 'rotateY(180deg)' }} />
|
<HandCoin className='align-self-center fill-boost mx-1' width={24} height={24} style={{ flex: '0 0 24px', transform: 'rotateY(180deg)' }} />
|
||||||
<div className='ms-2'>
|
<div className='ms-2'>
|
||||||
<NoteHeader color='boost' big>
|
<div className='fw-bold text-boost'>
|
||||||
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in rewards<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{time}</small>
|
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in rewards<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{time}</small>
|
||||||
</NoteHeader>
|
</div>
|
||||||
{n.sources &&
|
{n.sources &&
|
||||||
<div style={{ fontSize: '80%', color: 'var(--theme-grey)' }}>
|
<div style={{ fontSize: '80%', color: 'var(--theme-grey)' }}>
|
||||||
{n.sources.posts > 0 && <span>{numWithUnits(n.sources.posts, { abbreviate: false })} for top posts</span>}
|
{n.sources.posts > 0 && <span>{numWithUnits(n.sources.posts, { abbreviate: false })} for top posts</span>}
|
||||||
@ -208,29 +184,7 @@ function EarnNotification ({ n }) {
|
|||||||
{n.sources.tipComments > 0 && <span>{(n.sources.comments > 0 || n.sources.posts > 0 || n.sources.tipPosts > 0) && ' \\ '}{numWithUnits(n.sources.tipComments, { abbreviate: false })} for zapping top comments early</span>}
|
{n.sources.tipComments > 0 && <span>{(n.sources.comments > 0 || n.sources.posts > 0 || n.sources.tipPosts > 0) && ' \\ '}{numWithUnits(n.sources.tipComments, { abbreviate: false })} for zapping top comments early</span>}
|
||||||
</div>}
|
</div>}
|
||||||
<div style={{ lineHeight: '140%' }}>
|
<div style={{ lineHeight: '140%' }}>
|
||||||
SN distributes the sats it earns to top stackers like you daily. The top stackers make the top posts and comments or zap the top posts and comments early and generously. View the rewards pool and make a donation <Link href='/rewards'>here</Link>.
|
SN distributes the sats it earns back to its best stackers. These sats come from <Link href='/~jobs'>jobs</Link>, boosts, posting fees, and donations. You can see the rewards pool and make a donation <Link href='/rewards'>here</Link>.
|
||||||
</div>
|
|
||||||
<small className='text-muted ms-1 pb-1 fw-normal'>click for details</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReferralReward ({ n }) {
|
|
||||||
return (
|
|
||||||
<div className='d-flex'>
|
|
||||||
<UserAdd className='align-self-center fill-success mx-1' width={24} height={24} style={{ flex: '0 0 24px', transform: 'rotateY(180deg)' }} />
|
|
||||||
<div className='ms-2'>
|
|
||||||
<NoteHeader color='success' big>
|
|
||||||
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in referral rewards<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{dayMonthYear(new Date(n.sortTime))}</small>
|
|
||||||
</NoteHeader>
|
|
||||||
{n.sources &&
|
|
||||||
<div style={{ fontSize: '80%', color: 'var(--theme-grey)' }}>
|
|
||||||
{n.sources.forever > 0 && <span>{numWithUnits(n.sources.forever, { abbreviate: false })} for stackers joining because of you</span>}
|
|
||||||
{n.sources.oneDay > 0 && <span>{n.sources.forever > 0 && ' \\ '}{numWithUnits(n.sources.oneDay, { abbreviate: false })} for stackers referred to content by you today</span>}
|
|
||||||
</div>}
|
|
||||||
<div style={{ lineHeight: '140%' }}>
|
|
||||||
SN gives referral rewards to stackers like you for referring the top stackers daily. You refer stackers when they visit your posts, comments, profile, territory, or if they visit SN through your referral links.
|
|
||||||
</div>
|
</div>
|
||||||
<small className='text-muted ms-1 pb-1 fw-normal'>click for details</small>
|
<small className='text-muted ms-1 pb-1 fw-normal'>click for details</small>
|
||||||
</div>
|
</div>
|
||||||
@ -240,9 +194,9 @@ function ReferralReward ({ n }) {
|
|||||||
|
|
||||||
function RevenueNotification ({ n }) {
|
function RevenueNotification ({ n }) {
|
||||||
return (
|
return (
|
||||||
<div className='d-flex'>
|
<div className='d-flex ms-2 py-1'>
|
||||||
<BountyIcon className='align-self-center fill-success mx-1' width={24} height={24} style={{ flex: '0 0 24px' }} />
|
<BountyIcon className='align-self-center fill-success mx-1' width={24} height={24} style={{ flex: '0 0 24px' }} />
|
||||||
<div className=' pb-1'>
|
<div className='ms-2 pb-1'>
|
||||||
<div className='fw-bold text-success'>
|
<div className='fw-bold text-success'>
|
||||||
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in territory revenue<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in territory revenue<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
</div>
|
</div>
|
||||||
@ -257,7 +211,7 @@ function RevenueNotification ({ n }) {
|
|||||||
function SubStatus ({ n }) {
|
function SubStatus ({ n }) {
|
||||||
const dueDate = nextBillingWithGrace(n.sub)
|
const dueDate = nextBillingWithGrace(n.sub)
|
||||||
return (
|
return (
|
||||||
<div className={`fw-bold text-${n.sub.status === 'ACTIVE' ? 'success' : 'danger'} `}>
|
<div className={`fw-bold text-${n.sub.status === 'ACTIVE' ? 'success' : 'danger'} ms-2`}>
|
||||||
{n.sub.status === 'ACTIVE'
|
{n.sub.status === 'ACTIVE'
|
||||||
? 'your territory is active again'
|
? 'your territory is active again'
|
||||||
: (n.sub.status === 'GRACE'
|
: (n.sub.status === 'GRACE'
|
||||||
@ -271,14 +225,14 @@ function SubStatus ({ n }) {
|
|||||||
function Invitification ({ n }) {
|
function Invitification ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteHeader color='secondary'>
|
<small className='fw-bold text-secondary ms-2'>
|
||||||
your invite has been redeemed by
|
your invite has been redeemed by
|
||||||
{numWithUnits(n.invite.invitees.length, {
|
{numWithUnits(n.invite.invitees.length, {
|
||||||
abbreviate: false,
|
abbreviate: false,
|
||||||
unitSingular: 'stacker',
|
unitSingular: 'stacker',
|
||||||
unitPlural: 'stackers'
|
unitPlural: 'stackers'
|
||||||
})}
|
})}
|
||||||
</NoteHeader>
|
</small>
|
||||||
<div className='ms-4 me-2 mt-1'>
|
<div className='ms-4 me-2 mt-1'>
|
||||||
<Invite
|
<Invite
|
||||||
invite={n.invite} active={
|
invite={n.invite} active={
|
||||||
@ -296,23 +250,25 @@ function NostrZap ({ n }) {
|
|||||||
const { npub, content, note } = nostrZapDetails(nostr)
|
const { npub, content, note } = nostrZapDetails(nostr)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fw-bold text-nostr'>
|
<>
|
||||||
<NostrIcon width={24} height={24} className='fill-nostr me-1' />{numWithUnits(n.earnedSats)} zap from
|
<div className='fw-bold text-nostr ms-2 py-1'>
|
||||||
{// eslint-disable-next-line
|
<NostrIcon width={24} height={24} className='fill-nostr me-1' />{numWithUnits(n.earnedSats)} zap from
|
||||||
|
{// eslint-disable-next-line
|
||||||
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://njump.me/${npub}`} rel={UNKNOWN_LINK_REL}>
|
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://njump.me/${npub}`} rel={UNKNOWN_LINK_REL}>
|
||||||
{npub.slice(0, 10)}...
|
{npub.slice(0, 10)}...
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
on {note
|
on {note
|
||||||
? (
|
? (
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://njump.me/${note}`} rel={UNKNOWN_LINK_REL}>
|
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://njump.me/${note}`} rel={UNKNOWN_LINK_REL}>
|
||||||
{note.slice(0, 12)}...
|
{note.slice(0, 12)}...
|
||||||
</Link>)
|
</Link>)
|
||||||
: 'nostr'}
|
: 'nostr'}
|
||||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
{content && <small className='d-block ms-4 ps-1 mt-1 mb-1 text-muted fw-normal'><Text>{content}</Text></small>}
|
{content && <small className='d-block ms-4 ps-1 mt-1 mb-1 text-muted fw-normal'><Text>{content}</Text></small>}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,7 +286,7 @@ function InvoicePaid ({ n }) {
|
|||||||
if (id) payerSig += id
|
if (id) payerSig += id
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className='fw-bold text-info'>
|
<div className='fw-bold text-info ms-2 py-1'>
|
||||||
<Check className='fill-info me-1' />{numWithUnits(n.earnedSats, { abbreviate: false, unitSingular: 'sat was', unitPlural: 'sats were' })} deposited in your account
|
<Check className='fill-info me-1' />{numWithUnits(n.earnedSats, { abbreviate: false, unitSingular: 'sat was', unitPlural: 'sats were' })} deposited in your account
|
||||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
{n.invoice.comment &&
|
{n.invoice.comment &&
|
||||||
@ -413,23 +369,23 @@ function Invoicification ({ n: { invoice, sortTime } }) {
|
|||||||
({ id: invoiceId, actionState: invoiceActionState } = invoice.itemAct.invoice)
|
({ id: invoiceId, actionState: invoiceActionState } = invoice.itemAct.invoice)
|
||||||
}
|
}
|
||||||
|
|
||||||
let colorClass = 'info'
|
let colorClass = 'text-info'
|
||||||
switch (invoiceActionState) {
|
switch (invoiceActionState) {
|
||||||
case 'FAILED':
|
case 'FAILED':
|
||||||
actionString += 'failed'
|
actionString += 'failed'
|
||||||
colorClass = 'warning'
|
colorClass = 'text-warning'
|
||||||
break
|
break
|
||||||
case 'PAID':
|
case 'PAID':
|
||||||
actionString += 'paid'
|
actionString += 'paid'
|
||||||
colorClass = 'success'
|
colorClass = 'text-success'
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
actionString += 'pending'
|
actionString += 'pending'
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='px-2'>
|
||||||
<NoteHeader color={colorClass}>
|
<small className={`fw-bold ${colorClass} d-inline-flex align-items-center my-1`}>
|
||||||
{actionString}
|
{actionString}
|
||||||
<span className='ms-1 text-muted fw-light'> {numWithUnits(invoice.satsRequested)}</span>
|
<span className='ms-1 text-muted fw-light'> {numWithUnits(invoice.satsRequested)}</span>
|
||||||
<span className={invoiceActionState === 'FAILED' ? 'visible' : 'invisible'}>
|
<span className={invoiceActionState === 'FAILED' ? 'visible' : 'invisible'}>
|
||||||
@ -449,15 +405,25 @@ function Invoicification ({ n: { invoice, sortTime } }) {
|
|||||||
</Button>
|
</Button>
|
||||||
<span className='text-muted ms-2 fw-normal' suppressHydrationWarning>{timeSince(new Date(sortTime))}</span>
|
<span className='text-muted ms-2 fw-normal' suppressHydrationWarning>{timeSince(new Date(sortTime))}</span>
|
||||||
</span>
|
</span>
|
||||||
</NoteHeader>
|
</small>
|
||||||
<NoteItem item={invoice.item} />
|
<div>
|
||||||
|
{invoice.item.title
|
||||||
|
? <Item item={invoice.item} />
|
||||||
|
: (
|
||||||
|
<div className='pb-2'>
|
||||||
|
<RootProvider root={invoice.item.root}>
|
||||||
|
<Comment item={invoice.item} noReply includeParent clickToContext />
|
||||||
|
</RootProvider>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function WithdrawlPaid ({ n }) {
|
function WithdrawlPaid ({ n }) {
|
||||||
return (
|
return (
|
||||||
<div className='fw-bold text-info'>
|
<div className='fw-bold text-info ms-2 py-1'>
|
||||||
<Check className='fill-info me-1' />{numWithUnits(n.earnedSats, { abbreviate: false, unitSingular: 'sat was', unitPlural: 'sats were' })} withdrawn from your account
|
<Check className='fill-info me-1' />{numWithUnits(n.earnedSats, { abbreviate: false, unitSingular: 'sat was', unitPlural: 'sats were' })} withdrawn from your account
|
||||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
{n.withdrawl.autoWithdraw && <Badge className={styles.badge} bg={null}>autowithdraw</Badge>}
|
{n.withdrawl.autoWithdraw && <Badge className={styles.badge} bg={null}>autowithdraw</Badge>}
|
||||||
@ -467,8 +433,8 @@ function WithdrawlPaid ({ n }) {
|
|||||||
|
|
||||||
function Referral ({ n }) {
|
function Referral ({ n }) {
|
||||||
return (
|
return (
|
||||||
<small className='fw-bold text-success'>
|
<small className='fw-bold text-secondary ms-2'>
|
||||||
<UserAdd className='fill-success me-2' height={21} width={21} style={{ transform: 'rotateY(180deg)' }} />someone joined SN because of you
|
someone joined via one of your referral links
|
||||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
</small>
|
</small>
|
||||||
)
|
)
|
||||||
@ -489,15 +455,25 @@ function Votification ({ n }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteHeader color='success'>
|
<small className='fw-bold text-success d-inline-block ms-2 my-1' style={{ lineHeight: '1.25' }}>
|
||||||
your {n.item.title ? 'post' : 'reply'} stacked {numWithUnits(n.earnedSats, { abbreviate: false })}
|
your {n.item.title ? 'post' : 'reply'} stacked {numWithUnits(n.earnedSats, { abbreviate: false })}
|
||||||
{n.item.forwards?.length > 0 &&
|
{n.item.forwards?.length > 0 &&
|
||||||
<>
|
<>
|
||||||
{' '}and forwarded {numWithUnits(forwardedSats, { abbreviate: false })} to{' '}
|
{' '}and forwarded {numWithUnits(forwardedSats, { abbreviate: false })} to{' '}
|
||||||
<ForwardedUsers />
|
<ForwardedUsers />
|
||||||
</>}
|
</>}
|
||||||
</NoteHeader>
|
</small>
|
||||||
<NoteItem item={n.item} />
|
<div>
|
||||||
|
{n.item.title
|
||||||
|
? <Item item={n.item} />
|
||||||
|
: (
|
||||||
|
<div className='pb-2'>
|
||||||
|
<RootProvider root={n.item.root}>
|
||||||
|
<Comment item={n.item} noReply includeParent clickToContext />
|
||||||
|
</RootProvider>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -505,10 +481,20 @@ function Votification ({ n }) {
|
|||||||
function ForwardedVotification ({ n }) {
|
function ForwardedVotification ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteHeader color='success'>
|
<small className='fw-bold text-success d-inline-block ms-2 my-1' style={{ lineHeight: '1.25' }}>
|
||||||
you were forwarded {numWithUnits(n.earnedSats, { abbreviate: false })} from
|
you were forwarded {numWithUnits(n.earnedSats, { abbreviate: false })} from
|
||||||
</NoteHeader>
|
</small>
|
||||||
<NoteItem item={n.item} />
|
<div>
|
||||||
|
{n.item.title
|
||||||
|
? <Item item={n.item} />
|
||||||
|
: (
|
||||||
|
<div className='pb-2'>
|
||||||
|
<RootProvider root={n.item.root}>
|
||||||
|
<Comment item={n.item} noReply includeParent clickToContext />
|
||||||
|
</RootProvider>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -516,10 +502,19 @@ function ForwardedVotification ({ n }) {
|
|||||||
function Mention ({ n }) {
|
function Mention ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteHeader color='info'>
|
<small className='fw-bold text-info ms-2'>
|
||||||
you were mentioned in
|
you were mentioned in
|
||||||
</NoteHeader>
|
</small>
|
||||||
<NoteItem item={n.item} />
|
<div>
|
||||||
|
{n.item.title
|
||||||
|
? <Item item={n.item} />
|
||||||
|
: (
|
||||||
|
<div className='pb-2'>
|
||||||
|
<RootProvider root={n.item.root}>
|
||||||
|
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
|
||||||
|
</RootProvider>
|
||||||
|
</div>)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -527,10 +522,17 @@ function Mention ({ n }) {
|
|||||||
function ItemMention ({ n }) {
|
function ItemMention ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteHeader color='info'>
|
<small className='fw-bold text-info ms-2'>
|
||||||
your item was mentioned in
|
your item was mentioned in
|
||||||
</NoteHeader>
|
</small>
|
||||||
<NoteItem item={n.item} />
|
{n.item?.title
|
||||||
|
? <div className='ps-2'><Item item={n.item} /></div>
|
||||||
|
: (
|
||||||
|
<div className='pb-2'>
|
||||||
|
<RootProvider root={n.item.root}>
|
||||||
|
<Comment item={n.item} noReply includeParent rootText='replying on:' clickToContext />
|
||||||
|
</RootProvider>
|
||||||
|
</div>)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -538,29 +540,49 @@ function ItemMention ({ n }) {
|
|||||||
function JobChanged ({ n }) {
|
function JobChanged ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteHeader color={n.item.status === 'ACTIVE' ? 'success' : 'boost'}>
|
<small className={`fw-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ms-1`}>
|
||||||
{n.item.status === 'ACTIVE'
|
{n.item.status === 'ACTIVE'
|
||||||
? 'your job is active again'
|
? 'your job is active again'
|
||||||
: (n.item.status === 'NOSATS'
|
: (n.item.status === 'NOSATS'
|
||||||
? 'your job promotion ran out of sats'
|
? 'your job promotion ran out of sats'
|
||||||
: 'your job has been stopped')}
|
: 'your job has been stopped')}
|
||||||
</NoteHeader>
|
</small>
|
||||||
<ItemJob item={n.item} />
|
<ItemJob item={n.item} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Reply ({ n }) {
|
function Reply ({ n }) {
|
||||||
return <NoteItem item={n.item} />
|
return (
|
||||||
|
<div className='py-2'>
|
||||||
|
{n.item.title
|
||||||
|
? <Item item={n.item} />
|
||||||
|
: (
|
||||||
|
<div className='pb-2'>
|
||||||
|
<RootProvider root={n.item.root}>
|
||||||
|
<Comment item={n.item} noReply includeParent clickToContext rootText='replying on:' />
|
||||||
|
</RootProvider>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FollowActivity ({ n }) {
|
function FollowActivity ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteHeader color='info'>
|
<small className='fw-bold text-info ms-2'>
|
||||||
a stacker you subscribe to {n.item.parentId ? 'commented' : 'posted'}
|
a stacker you subscribe to {n.item.parentId ? 'commented' : 'posted'}
|
||||||
</NoteHeader>
|
</small>
|
||||||
<NoteItem item={n.item} />
|
{n.item.title
|
||||||
|
? <div className='ms-2'><Item item={n.item} /></div>
|
||||||
|
: (
|
||||||
|
<div className='pb-2'>
|
||||||
|
<RootProvider root={n.item.root}>
|
||||||
|
<Comment item={n.item} noReply includeParent clickToContext rootText='replying on:' />
|
||||||
|
</RootProvider>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -568,11 +590,11 @@ function FollowActivity ({ n }) {
|
|||||||
function TerritoryPost ({ n }) {
|
function TerritoryPost ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteHeader color='info'>
|
<small className='fw-bold text-info ms-2'>
|
||||||
new post in ~{n.item.sub.name}
|
new post in ~{n.item.sub.name}
|
||||||
</NoteHeader>
|
</small>
|
||||||
<div>
|
<div className='ps-2'>
|
||||||
<Item item={n.item} itemClassName='pt-0' />
|
<Item item={n.item} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -580,20 +602,28 @@ function TerritoryPost ({ n }) {
|
|||||||
|
|
||||||
function TerritoryTransfer ({ n }) {
|
function TerritoryTransfer ({ n }) {
|
||||||
return (
|
return (
|
||||||
<div className='fw-bold text-info '>
|
<>
|
||||||
~{n.sub.name} was transferred to you
|
<div className='fw-bold text-info ms-2'>
|
||||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
~{n.sub.name} was transferred to you
|
||||||
</div>
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Reminder ({ n }) {
|
function Reminder ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteHeader color='info'>
|
<small className='fw-bold text-info ms-2'>you asked to be reminded of this {n.item.title ? 'post' : 'comment'}</small>
|
||||||
you asked to be reminded of this {n.item.title ? 'post' : 'comment'}
|
{n.item.title
|
||||||
</NoteHeader>
|
? <div className='ms-2'><Item item={n.item} /></div>
|
||||||
<NoteItem item={n.item} />
|
: (
|
||||||
|
<div className='pb-2'>
|
||||||
|
<RootProvider root={n.item.root}>
|
||||||
|
<Comment item={n.item} noReply includeParent clickToContext rootText='replying on:' />
|
||||||
|
</RootProvider>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,14 @@
|
|||||||
.fresh {
|
.fresh {
|
||||||
|
background-color: rgba(128, 128, 128, 0.1);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border: solid 1px var(--theme-note-fresh);
|
|
||||||
border-bottom: 0;
|
|
||||||
border-top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fresh:not(.fresh ~ .fresh) {
|
.fresh:not(.fresh ~ .fresh) {
|
||||||
border-top-left-radius: .4rem;
|
border-top-left-radius: .4rem;
|
||||||
border-top-right-radius: .4rem;
|
border-top-right-radius: .4rem;
|
||||||
border-top: solid 1px var(--theme-note-fresh);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fresh:has(+ :not(.fresh)):has(+ :not(.reply)),
|
|
||||||
.fresh:not(.reply):has(+ :not(.fresh)) {
|
|
||||||
border-bottom-left-radius: .4rem;
|
|
||||||
border-bottom-right-radius: .4rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fresh:has(+ :not(.fresh)) {
|
.fresh:has(+ :not(.fresh)) {
|
||||||
border-bottom: solid 1px var(--theme-note-fresh);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply {
|
|
||||||
border-radius: 0;
|
|
||||||
background-color: var(--theme-note-reply);
|
|
||||||
border-bottom: 0;
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply:hover {
|
|
||||||
background-color: var(--theme-clickToContextColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply:not(.fresh):not(.reply + .reply) {
|
|
||||||
border-top-left-radius: .4rem;
|
|
||||||
border-top-right-radius: .4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply:not(.fresh):has(+ :not(.reply)) {
|
|
||||||
border-bottom-left-radius: .4rem;
|
border-bottom-left-radius: .4rem;
|
||||||
border-bottom-right-radius: .4rem;
|
border-bottom-right-radius: .4rem;
|
||||||
}
|
}
|
||||||
@ -63,10 +34,3 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.noteHeader {
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1.25;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
@ -15,7 +15,7 @@ export default function RecentHeader ({ type, sub }) {
|
|||||||
|
|
||||||
type ||= router.query.type || type || 'posts'
|
type ||= router.query.type || type || 'posts'
|
||||||
return (
|
return (
|
||||||
<div className='text-muted fw-bold my-1 d-flex justify-content-start align-items-center'>
|
<div className='text-muted fw-bold mt-1 mb-3 d-flex justify-content-start align-items-center'>
|
||||||
<Select
|
<Select
|
||||||
groupClassName='mb-2'
|
groupClassName='mb-2'
|
||||||
className='w-auto'
|
className='w-auto'
|
||||||
|
@ -113,7 +113,7 @@ export default forwardRef(function Reply ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{replyOpen
|
{replyOpen
|
||||||
? <div className='p-3' />
|
? <div className={styles.replyButtons} />
|
||||||
: (
|
: (
|
||||||
<div className={styles.replyButtons}>
|
<div className={styles.replyButtons}>
|
||||||
<div
|
<div
|
||||||
|
@ -5,21 +5,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.replyButtons {
|
.replyButtons {
|
||||||
font-size: 80%;
|
font-size: 75%;
|
||||||
color: var(--theme-grey);
|
color: var(--theme-grey);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: .25rem 0 .8rem 0;
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.replyButtons > * {
|
|
||||||
padding-top: .4rem;
|
|
||||||
padding-bottom: .8rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton .input {
|
.skeleton .input {
|
||||||
background-color: var(--theme-grey);
|
background-color: var(--theme-grey);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -81,7 +81,7 @@ export default function TerritoryHeader ({ sub }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TerritoryPaymentDue sub={sub} />
|
<TerritoryPaymentDue sub={sub} />
|
||||||
<div className='mb-2 mt-1'>
|
<div className='mb-3'>
|
||||||
<div>
|
<div>
|
||||||
<TerritoryDetails sub={sub}>
|
<TerritoryDetails sub={sub}>
|
||||||
<div className='d-flex my-2 justify-content-end'>
|
<div className='d-flex my-2 justify-content-end'>
|
||||||
|
@ -46,7 +46,7 @@ export default function TopHeader ({ sub, cat }) {
|
|||||||
initial={{ what, by, when, from: '', to: '' }}
|
initial={{ what, by, when, from: '', to: '' }}
|
||||||
onSubmit={top}
|
onSubmit={top}
|
||||||
>
|
>
|
||||||
<div className='text-muted fw-bold my-1 d-flex align-items-center flex-wrap'>
|
<div className='text-muted fw-bold mt-1 mb-3 d-flex align-items-center flex-wrap'>
|
||||||
<div className='text-muted fw-bold mb-2 d-flex align-items-center'>
|
<div className='text-muted fw-bold mb-2 d-flex align-items-center'>
|
||||||
<Select
|
<Select
|
||||||
groupClassName='me-2 mb-0'
|
groupClassName='me-2 mb-0'
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
.upvoteWrapper {
|
.upvoteWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-right: .2rem;
|
margin-right: .2rem;
|
||||||
padding-left: .2rem;
|
padding-left: .2rem;
|
||||||
margin-left: -.4rem;
|
margin-left: -.4rem;
|
||||||
}
|
}
|
||||||
|
@ -99,15 +99,6 @@ export const NOTIFICATIONS = gql`
|
|||||||
tipComments
|
tipComments
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
... on ReferralReward {
|
|
||||||
id
|
|
||||||
sortTime
|
|
||||||
earnedSats
|
|
||||||
sources {
|
|
||||||
forever
|
|
||||||
oneDay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
... on Referral {
|
... on Referral {
|
||||||
id
|
id
|
||||||
sortTime
|
sortTime
|
||||||
|
@ -25,7 +25,7 @@ export default function Related ({ ssrData }) {
|
|||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Item item={item} />
|
<Item item={item} />
|
||||||
<div className='fw-bold mt-2'>related</div>
|
<div className='fw-bold my-2'>related</div>
|
||||||
<Items
|
<Items
|
||||||
ssrData={ssrData}
|
ssrData={ssrData}
|
||||||
query={RELATED_ITEMS}
|
query={RELATED_ITEMS}
|
||||||
|
@ -21,10 +21,14 @@ const REFERRALS = gql`
|
|||||||
query Referrals($when: String!, $from: String, $to: String)
|
query Referrals($when: String!, $from: String, $to: String)
|
||||||
{
|
{
|
||||||
referrals(when: $when, from: $from, to: $to) {
|
referrals(when: $when, from: $from, to: $to) {
|
||||||
time
|
totalSats
|
||||||
data {
|
totalReferrals
|
||||||
name
|
stats {
|
||||||
value
|
time
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
@ -50,12 +54,7 @@ export default function Referrals ({ ssrData }) {
|
|||||||
const { data } = useQuery(REFERRALS, { variables: { when: router.query.when, from: router.query.from, to: router.query.to } })
|
const { data } = useQuery(REFERRALS, { variables: { when: router.query.when, from: router.query.from, to: router.query.to } })
|
||||||
if (!data && !ssrData) return <PageLoading />
|
if (!data && !ssrData) return <PageLoading />
|
||||||
|
|
||||||
const { referrals } = data || ssrData
|
const { referrals: { totalSats, totalReferrals, stats } } = data || ssrData
|
||||||
const totalSats = referrals.reduce(
|
|
||||||
(total, a) => total + a.data?.filter(d => d.name.endsWith('sats')).reduce(
|
|
||||||
(acc, d) => acc + d.value,
|
|
||||||
0),
|
|
||||||
0)
|
|
||||||
|
|
||||||
const when = router.query.when
|
const when = router.query.when
|
||||||
|
|
||||||
@ -63,7 +62,7 @@ export default function Referrals ({ ssrData }) {
|
|||||||
<CenterLayout footerLinks>
|
<CenterLayout footerLinks>
|
||||||
<div className='fw-bold text-muted text-center pt-5 pb-3 d-flex align-items-center justify-content-center flex-wrap'>
|
<div className='fw-bold text-muted text-center pt-5 pb-3 d-flex align-items-center justify-content-center flex-wrap'>
|
||||||
<h4 className='fw-bold text-muted text-center d-flex align-items-center justify-content-center'>
|
<h4 className='fw-bold text-muted text-center d-flex align-items-center justify-content-center'>
|
||||||
{numWithUnits(totalSats, { abbreviate: false })} in the last
|
{numWithUnits(totalReferrals, { unitPlural: 'referrals', unitSingular: 'referral' })} & {numWithUnits(totalSats, { abbreviate: false })} in the last
|
||||||
<Select
|
<Select
|
||||||
groupClassName='mb-0 mx-2'
|
groupClassName='mb-0 mx-2'
|
||||||
className='w-auto'
|
className='w-auto'
|
||||||
@ -92,13 +91,7 @@ export default function Referrals ({ ssrData }) {
|
|||||||
when={router.query.when}
|
when={router.query.when}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
<WhenComposedChart
|
<WhenComposedChart data={stats} lineNames={['sats']} barNames={['referrals']} barAxis='right' />
|
||||||
data={referrals}
|
|
||||||
areaNames={['referral sats', 'one day referral sats']}
|
|
||||||
barNames={['referrals', 'one day referrals']}
|
|
||||||
barAxis='right'
|
|
||||||
barStackId={1}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className='text-small pt-5 px-3 d-flex w-100 align-items-center'
|
className='text-small pt-5 px-3 d-flex w-100 align-items-center'
|
||||||
@ -113,16 +106,13 @@ export default function Referrals ({ ssrData }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ul className='py-3 text-muted'>
|
<ul className='py-3 text-muted'>
|
||||||
<li>earn 10% of a stacker's <Link href='/rewards'>rewards</Link> in perpetuity if they sign up from your referral links</li>
|
<li>{`appending /r/${me.name} to any SN link makes it a ref link`}
|
||||||
<li>in addition, earn 10% of a stacker's <Link href='/rewards'>rewards</Link> for the day if they follow your referral links the most that day</li>
|
|
||||||
<li>nearly all sn links are referral links:
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>your profile link is an implicit referral link</li>
|
<li>e.g. https://stacker.news/items/1/r/{me.name}</li>
|
||||||
<li>all links to post and comments are implicit referral links attributed to the OP</li>
|
|
||||||
<li>links to territories are implicit referral links attributed to the territory founder</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>appending /r/{me.name} to any SN link makes it a ref link to {me.name}</li>
|
<li>earn 21% of boost and job fees spent by referred stackers</li>
|
||||||
|
<li>earn 2.1% of all zaps received by referred stackers</li>
|
||||||
<li><Link href='/invites'>invite links</Link> are also implicitly referral links</li>
|
<li><Link href='/invites'>invite links</Link> are also implicitly referral links</li>
|
||||||
</ul>
|
</ul>
|
||||||
</CenterLayout>
|
</CenterLayout>
|
||||||
|
@ -110,13 +110,10 @@ export default function Rewards ({ ssrData }) {
|
|||||||
if (!dat) return <PageLoading />
|
if (!dat) return <PageLoading />
|
||||||
|
|
||||||
function EstimatedReward ({ rank }) {
|
function EstimatedReward ({ rank }) {
|
||||||
const referrerReward = Math.floor(total * proportions[rank - 1] * 0.2)
|
|
||||||
const reward = Math.floor(total * proportions[rank - 1]) - referrerReward
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='text-muted fst-italic'>
|
<div className='text-muted fst-italic'>
|
||||||
<small>
|
<small>
|
||||||
<span>estimated reward: {numWithUnits(reward)} <small className='fw-light'>(+ {numWithUnits(referrerReward)} to referrers)</small></span>
|
<span>estimated reward: {numWithUnits(Math.floor(total * proportions[rank - 1]))}</span>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -124,12 +121,11 @@ export default function Rewards ({ ssrData }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout footerLinks>
|
<Layout footerLinks>
|
||||||
<h4 className='pt-3 align-self-center text-reset'>
|
<Link className='text-reset align-self-center' href='/items/141924'>
|
||||||
<small className='text-muted'>rewards are sponsored by ...</small>
|
<h4 className='pt-3 text-start text-reset' style={{ lineHeight: 1.5, textDecoration: 'underline' }}>
|
||||||
<Link className='text-reset ms-2' href='/items/141924' style={{ lineHeight: 1.5, textDecoration: 'underline' }}>
|
rewards are sponsored by ... we are hiring
|
||||||
SN is hiring
|
</h4>
|
||||||
</Link>
|
</Link>
|
||||||
</h4>
|
|
||||||
<Row className='pb-3'>
|
<Row className='pb-3'>
|
||||||
<Col lg={leaderboard?.users && 5}>
|
<Col lg={leaderboard?.users && 5}>
|
||||||
<div
|
<div
|
||||||
|
@ -48,6 +48,7 @@ export default function Satistics ({ ssrData }) {
|
|||||||
|
|
||||||
const totalStacked = userStatsIncomingSats.reduce((total, a) => total + a.data?.reduce((acc, d) => acc + d.value, 0), 0)
|
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 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 (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
@ -58,8 +59,8 @@ export default function Satistics ({ ssrData }) {
|
|||||||
<UsageHeader pathname='satistics/graphs' />
|
<UsageHeader pathname='satistics/graphs' />
|
||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
<div className='d-flex row justify-content-between'>
|
<div className='d-flex row justify-content-between'>
|
||||||
<div className='col-md-6 mb-2'>
|
<div className='col-md-4 mb-2'>
|
||||||
<h4 className='w-100 text-center'>stacked</h4>
|
<h4>stacked</h4>
|
||||||
<div className='card'>
|
<div className='card'>
|
||||||
<div className='card-body'>
|
<div className='card-body'>
|
||||||
<SatisticsTooltip overlayText={numWithUnits(totalStacked, { abbreviate: false, format: true })}>
|
<SatisticsTooltip overlayText={numWithUnits(totalStacked, { abbreviate: false, format: true })}>
|
||||||
@ -70,8 +71,8 @@ export default function Satistics ({ ssrData }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-md-6 mb-2'>
|
<div className='col-md-4 mb-2'>
|
||||||
<h4 className='w-100 text-center'>spent</h4>
|
<h4>spent</h4>
|
||||||
<div className='card'>
|
<div className='card'>
|
||||||
<div className='card-body'>
|
<div className='card-body'>
|
||||||
<SatisticsTooltip overlayText={numWithUnits(totalSpent, { abbreviate: false, format: true })}>
|
<SatisticsTooltip overlayText={numWithUnits(totalSpent, { abbreviate: false, format: true })}>
|
||||||
@ -82,6 +83,16 @@ export default function Satistics ({ ssrData }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='col-md-4'>
|
||||||
|
<h4>actions</h4>
|
||||||
|
<div className='card'>
|
||||||
|
<div className='card-body'>
|
||||||
|
<h2 className='text-center mb-0 text-muted'>
|
||||||
|
{new Intl.NumberFormat().format(totalEngagement)}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='row mt-5'>
|
<div className='row mt-5'>
|
||||||
{userStatsIncomingSats.length > 0 &&
|
{userStatsIncomingSats.length > 0 &&
|
||||||
@ -99,7 +110,7 @@ export default function Satistics ({ ssrData }) {
|
|||||||
{userStatsActions.length > 0 &&
|
{userStatsActions.length > 0 &&
|
||||||
<div className='col-md-12'>
|
<div className='col-md-12'>
|
||||||
<div className='text-center text-muted fw-bold'>items</div>
|
<div className='text-center text-muted fw-bold'>items</div>
|
||||||
<WhenComposedChart data={userStatsActions} areaNames={['posts', 'comments']} areaAxis='left' lineNames={['territories', 'referrals', 'one day referrals']} lineAxis='right' />
|
<WhenComposedChart data={userStatsActions} areaNames={['posts', 'comments']} areaAxis='left' lineNames={['territories', 'referrals']} lineAxis='right' />
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
ALTER TYPE "EarnType" ADD VALUE 'FOREVER_REFERRAL';
|
|
||||||
ALTER TYPE "EarnType" ADD VALUE 'ONE_DAY_REFERRAL';
|
|
||||||
|
|
||||||
-- delete attributing one day referrals to pages
|
|
||||||
DELETE FROM "OneDayReferral"
|
|
||||||
WHERE "typeId" IN (
|
|
||||||
SELECT id::text
|
|
||||||
FROM users
|
|
||||||
WHERE name IN (
|
|
||||||
'api', 'auth', 'day', 'invites', 'invoices', 'referrals', 'rewards',
|
|
||||||
'satistics', 'settings', 'stackers', 'wallet', 'withdrawals', '404', '500',
|
|
||||||
'email', 'live', 'login', 'notifications', 'offline', 'search', 'share',
|
|
||||||
'signup', 'territory', 'recent', 'top', 'edit', 'post', 'rss', 'saloon',
|
|
||||||
'faq', 'story', 'privacy', 'copyright', 'tos', 'changes', 'guide', 'daily',
|
|
||||||
'anon', 'ad'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- delete attributing forever referrals to pages
|
|
||||||
UPDATE users SET "referrerId" = NULL
|
|
||||||
WHERE "referrerId" IN (
|
|
||||||
SELECT id
|
|
||||||
FROM users
|
|
||||||
WHERE name IN (
|
|
||||||
'api', 'auth', 'day', 'invites', 'invoices', 'referrals', 'rewards',
|
|
||||||
'satistics', 'settings', 'stackers', 'wallet', 'withdrawals', '404', '500',
|
|
||||||
'email', 'live', 'login', 'notifications', 'offline', 'search', 'share',
|
|
||||||
'signup', 'territory', 'recent', 'top', 'edit', 'post', 'rss', 'saloon',
|
|
||||||
'faq', 'story', 'privacy', 'copyright', 'tos', 'changes', 'guide', 'daily',
|
|
||||||
'anon', 'ad'
|
|
||||||
)
|
|
||||||
);
|
|
@ -1,223 +0,0 @@
|
|||||||
-- add referrals to user stats and more fields while we're at it
|
|
||||||
DROP FUNCTION IF EXISTS user_stats(timestamp without time zone,timestamp without time zone,interval,text) CASCADE;
|
|
||||||
CREATE OR REPLACE FUNCTION user_stats(min TIMESTAMP(3), max TIMESTAMP(3), ival INTERVAL, date_part TEXT)
|
|
||||||
RETURNS TABLE (
|
|
||||||
t TIMESTAMP(3), id INTEGER, comments BIGINT, posts BIGINT, territories BIGINT,
|
|
||||||
referrals BIGINT, one_day_referrals BIGINT, msats_tipped BIGINT, msats_rewards BIGINT,
|
|
||||||
msats_referrals BIGINT, msats_one_day_referrals BIGINT,
|
|
||||||
msats_revenue BIGINT, msats_stacked BIGINT, msats_fees BIGINT, msats_donated BIGINT,
|
|
||||||
msats_billing BIGINT, msats_zaps BIGINT, msats_spent BIGINT)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $$
|
|
||||||
DECLARE
|
|
||||||
min_utc TIMESTAMP(3) := timezone('utc', min AT TIME ZONE 'America/Chicago');
|
|
||||||
BEGIN
|
|
||||||
RETURN QUERY
|
|
||||||
SELECT period.t,
|
|
||||||
"userId" as id,
|
|
||||||
-- counts
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'COMMENT'))::BIGINT as comments,
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'POST'))::BIGINT as posts,
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'TERRITORY'))::BIGINT as territories,
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'REFERRAL'))::BIGINT as referrals,
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'ONE_DAY_REFERRAL_COUNT'))::BIGINT as one_day_referrals,
|
|
||||||
-- stacking
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'TIPPEE'))::BIGINT as msats_tipped,
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'EARN'))::BIGINT as msats_rewards,
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'REFERRAL_ACT' OR type = 'FOREVER_REFERRAL'))::BIGINT as msats_referrals,
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'ONE_DAY_REFERRAL'))::BIGINT as msats_one_day_referrals,
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'REVENUE'))::BIGINT as msats_revenue,
|
|
||||||
(sum(quantity) FILTER (WHERE type IN ('TIPPEE', 'EARN', 'REFERRAL_ACT', 'REVENUE', 'ONE_DAY_REFERRAL', 'FOREVER_REFERRAL')))::BIGINT as msats_stacked,
|
|
||||||
-- spending
|
|
||||||
(sum(quantity) FILTER (WHERE type IN ('BOOST', 'FEE', 'STREAM', 'POLL', 'DONT_LIKE_THIS')))::BIGINT as msats_fees,
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'DONATION'))::BIGINT as msats_donated,
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'BILLING'))::BIGINT as msats_billing,
|
|
||||||
(sum(quantity) FILTER (WHERE type = 'TIP'))::BIGINT as msats_zaps,
|
|
||||||
(sum(quantity) FILTER (WHERE type IN ('BOOST', 'TIP', 'FEE', 'STREAM', 'POLL', 'DONT_LIKE_THIS', 'DONATION', 'BILLING')))::BIGINT as msats_spent
|
|
||||||
FROM generate_series(min, max, ival) period(t)
|
|
||||||
LEFT JOIN
|
|
||||||
((SELECT "userId", msats as quantity, act::TEXT as type, created_at
|
|
||||||
FROM "ItemAct"
|
|
||||||
WHERE created_at >= min_utc
|
|
||||||
AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID'))
|
|
||||||
UNION ALL
|
|
||||||
(SELECT "userId", sats*1000 as quantity, 'DONATION' as type, created_at
|
|
||||||
FROM "Donation"
|
|
||||||
WHERE created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT "userId", 1 as quantity,
|
|
||||||
CASE WHEN "Item"."parentId" IS NULL THEN 'POST' ELSE 'COMMENT' END as type, created_at
|
|
||||||
FROM "Item"
|
|
||||||
WHERE created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT "referrerId" as "userId", 1 as quantity, 'REFERRAL' as type, created_at
|
|
||||||
FROM users
|
|
||||||
WHERE "referrerId" IS NOT NULL
|
|
||||||
AND created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
-- tips accounting for forwarding
|
|
||||||
(SELECT "Item"."userId", floor("ItemAct".msats * (1-COALESCE(sum("ItemForward".pct)/100.0, 0))) as quantity, 'TIPPEE' as type, "ItemAct".created_at
|
|
||||||
FROM "ItemAct"
|
|
||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
|
||||||
LEFT JOIN "ItemForward" on "ItemForward"."itemId" = "Item".id
|
|
||||||
WHERE "ItemAct".act = 'TIP'
|
|
||||||
AND "ItemAct".created_at >= min_utc
|
|
||||||
AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID')
|
|
||||||
GROUP BY "Item"."userId", "ItemAct".id, "ItemAct".msats, "ItemAct".created_at)
|
|
||||||
UNION ALL
|
|
||||||
-- tips where stacker is a forwardee
|
|
||||||
(SELECT "ItemForward"."userId", floor("ItemAct".msats*("ItemForward".pct/100.0)) as quantity, 'TIPPEE' as type, "ItemAct".created_at
|
|
||||||
FROM "ItemAct"
|
|
||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
|
||||||
JOIN "ItemForward" on "ItemForward"."itemId" = "Item".id
|
|
||||||
WHERE "ItemAct".act = 'TIP'
|
|
||||||
AND "ItemAct".created_at >= min_utc
|
|
||||||
AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID'))
|
|
||||||
UNION ALL
|
|
||||||
(SELECT "userId", msats as quantity, 'EARN' as type, created_at
|
|
||||||
FROM "Earn"
|
|
||||||
WHERE (type is NULL OR type NOT IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL'))
|
|
||||||
AND created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT "userId", msats as quantity, type::TEXT as type, created_at
|
|
||||||
FROM "Earn"
|
|
||||||
WHERE type IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL')
|
|
||||||
AND created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT "referrerId" as "userId", msats as quantity, 'REFERRAL_ACT' as type, created_at
|
|
||||||
FROM "ReferralAct"
|
|
||||||
WHERE created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT "userId", msats as quantity, type::TEXT as type, created_at
|
|
||||||
FROM "SubAct"
|
|
||||||
WHERE created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT "userId", 1 as quantity, 'TERRITORY' as type, created_at
|
|
||||||
FROM "Sub"
|
|
||||||
WHERE status <> 'STOPPED'
|
|
||||||
AND created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
-- for every referree, get the one day referrer on each day
|
|
||||||
(SELECT mode() WITHIN GROUP (ORDER BY "OneDayReferral"."referrerId") AS "userId", 1 as quantity,
|
|
||||||
'ONE_DAY_REFERRAL_COUNT' as type, max(created_at) AS created_at
|
|
||||||
FROM "OneDayReferral"
|
|
||||||
WHERE created_at >= min_utc
|
|
||||||
GROUP BY "refereeId", date_trunc('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')
|
|
||||||
HAVING mode() WITHIN GROUP (ORDER BY "OneDayReferral"."referrerId") IS NOT NULL)
|
|
||||||
) u ON period.t = date_trunc(date_part, u.created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')
|
|
||||||
GROUP BY "userId", period.t
|
|
||||||
ORDER BY period.t ASC;
|
|
||||||
END;
|
|
||||||
$$;
|
|
||||||
|
|
||||||
DROP MATERIALIZED VIEW IF EXISTS user_stats_hours;
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS user_stats_hours AS
|
|
||||||
SELECT (user_stats(min, max, '1 hour'::INTERVAL, 'hour')).* FROM last_24_hours;
|
|
||||||
|
|
||||||
DROP MATERIALIZED VIEW IF EXISTS user_stats_days;
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS user_stats_days AS
|
|
||||||
SELECT (user_stats(min, max, '1 day'::INTERVAL, 'day')).* FROM all_days;
|
|
||||||
|
|
||||||
DROP MATERIALIZED VIEW IF EXISTS user_stats_months;
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS user_stats_months AS
|
|
||||||
SELECT (user_stats(min, max, '1 month'::INTERVAL, 'month')).* FROM all_months;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS user_stats_months_idx ON user_stats_months(t, id);
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS user_stats_days_idx ON user_stats_days(t, id);
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS user_stats_hours_idx ON user_stats_hours(t, id);
|
|
||||||
|
|
||||||
DROP FUNCTION IF EXISTS earn(user_id INTEGER, earn_msats BIGINT, created_at TIMESTAMP(3),
|
|
||||||
type "EarnType", type_id INTEGER, rank INTEGER);
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION stacking_growth(min TIMESTAMP(3), max TIMESTAMP(3), ival INTERVAL, date_part TEXT)
|
|
||||||
RETURNS TABLE (t TIMESTAMP(3), rewards BIGINT, posts BIGINT, comments BIGINT, referrals BIGINT, territories BIGINT)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $$
|
|
||||||
DECLARE
|
|
||||||
min_utc TIMESTAMP(3) := timezone('utc', min AT TIME ZONE 'America/Chicago');
|
|
||||||
BEGIN
|
|
||||||
RETURN QUERY
|
|
||||||
SELECT period.t,
|
|
||||||
coalesce(floor(sum(airdrop)/1000),0)::BIGINT as rewards,
|
|
||||||
coalesce(floor(sum(post)/1000),0)::BIGINT as posts,
|
|
||||||
coalesce(floor(sum(comment)/1000),0)::BIGINT as comments,
|
|
||||||
coalesce(floor(sum(referral)/1000),0)::BIGINT as referrals,
|
|
||||||
coalesce(floor(sum(revenue)/1000),0)::BIGINT as territories
|
|
||||||
FROM generate_series(min, max, ival) period(t)
|
|
||||||
LEFT JOIN
|
|
||||||
((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 "ItemAct".msats ELSE 0 END as post,
|
|
||||||
0 as referral,
|
|
||||||
0 as revenue
|
|
||||||
FROM "ItemAct"
|
|
||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
|
||||||
WHERE "ItemAct".act = 'TIP'
|
|
||||||
AND "ItemAct".created_at >= min_utc
|
|
||||||
AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID'))
|
|
||||||
UNION ALL
|
|
||||||
(SELECT created_at, 0 as airdrop, 0 as post, 0 as comment, msats as referral, 0 as revenue
|
|
||||||
FROM "ReferralAct"
|
|
||||||
WHERE created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT created_at, msats as airdrop, 0 as post, 0 as comment, 0 as referral, 0 as revenue
|
|
||||||
FROM "Earn"
|
|
||||||
WHERE ("Earn".type is NULL OR "Earn".type NOT IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL'))
|
|
||||||
AND created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT created_at, 0 as airdrop, 0 as post, 0 as comment, msats as referral, 0 as revenue
|
|
||||||
FROM "Earn"
|
|
||||||
WHERE "Earn".type IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL')
|
|
||||||
AND created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT created_at, 0 as airdrop, 0 as post, 0 as comment, 0 as referral, msats as revenue
|
|
||||||
FROM "SubAct"
|
|
||||||
WHERE type = 'REVENUE'
|
|
||||||
AND created_at >= min_utc)
|
|
||||||
) u ON period.t = date_trunc(date_part, u.created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')
|
|
||||||
GROUP BY period.t
|
|
||||||
ORDER BY period.t ASC;
|
|
||||||
END;
|
|
||||||
$$;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION stackers_growth(min TIMESTAMP(3), max TIMESTAMP(3), ival INTERVAL, date_part TEXT)
|
|
||||||
RETURNS TABLE (t TIMESTAMP(3), "userId" INT, type TEXT)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $$
|
|
||||||
DECLARE
|
|
||||||
min_utc TIMESTAMP(3) := timezone('utc', min AT TIME ZONE 'America/Chicago');
|
|
||||||
BEGIN
|
|
||||||
RETURN QUERY
|
|
||||||
SELECT period.t, u."userId", u.type
|
|
||||||
FROM generate_series(min, max, ival) period(t)
|
|
||||||
LEFT JOIN
|
|
||||||
((SELECT "ItemAct".created_at, "Item"."userId", CASE WHEN "Item"."parentId" IS NULL THEN 'POST' ELSE 'COMMENT' END as type
|
|
||||||
FROM "ItemAct"
|
|
||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
|
||||||
WHERE "ItemAct".act = 'TIP'
|
|
||||||
AND "ItemAct".created_at >= min_utc
|
|
||||||
AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID'))
|
|
||||||
UNION ALL
|
|
||||||
(SELECT created_at, "Earn"."userId", 'EARN' as type
|
|
||||||
FROM "Earn"
|
|
||||||
WHERE ("Earn".type is NULL OR "Earn".type NOT IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL'))
|
|
||||||
AND created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT created_at, "ReferralAct"."referrerId" as "userId", 'REFERRAL' as type
|
|
||||||
FROM "ReferralAct"
|
|
||||||
WHERE created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT created_at, "Earn"."userId", 'REFERRAL' as type
|
|
||||||
FROM "Earn"
|
|
||||||
WHERE "Earn".type IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL')
|
|
||||||
AND created_at >= min_utc)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT created_at, "SubAct"."userId", 'REVENUE' as type
|
|
||||||
FROM "SubAct"
|
|
||||||
WHERE "SubAct".type = 'REVENUE'
|
|
||||||
AND created_at >= min_utc)
|
|
||||||
) u ON period.t = date_trunc(date_part, u.created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')
|
|
||||||
GROUP BY period.t, u."userId", u.type
|
|
||||||
ORDER BY period.t ASC;
|
|
||||||
END;
|
|
||||||
$$;
|
|
@ -1,20 +0,0 @@
|
|||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "Invoice_actionState_idx" ON "Invoice"("actionState");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "Invoice_actionType_idx" ON "Invoice"("actionType");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "Item_invoiceActionState_idx" ON "Item"("invoiceActionState");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "ItemAct_invoiceActionState_idx" ON "ItemAct"("invoiceActionState");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "PollBlindVote_invoiceActionState_idx" ON "PollBlindVote"("invoiceActionState");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "PollVote_invoiceActionState_idx" ON "PollVote"("invoiceActionState");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "Upload_invoiceActionState_idx" ON "Upload"("invoiceActionState");
|
|
@ -332,7 +332,6 @@ model Upload {
|
|||||||
@@index([createdAt], map: "Upload.created_at_index")
|
@@index([createdAt], map: "Upload.created_at_index")
|
||||||
@@index([userId], map: "Upload.userId_index")
|
@@index([userId], map: "Upload.userId_index")
|
||||||
@@index([invoiceId])
|
@@index([invoiceId])
|
||||||
@@index([invoiceActionState])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Earn {
|
model Earn {
|
||||||
@ -485,7 +484,6 @@ model Item {
|
|||||||
@@index([weightedDownVotes], map: "Item.weightedDownVotes_index")
|
@@index([weightedDownVotes], map: "Item.weightedDownVotes_index")
|
||||||
@@index([weightedVotes], map: "Item.weightedVotes_index")
|
@@index([weightedVotes], map: "Item.weightedVotes_index")
|
||||||
@@index([invoiceId])
|
@@index([invoiceId])
|
||||||
@@index([invoiceActionState])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// we use this to denormalize a user's aggregated interactions (zaps) with an item
|
// we use this to denormalize a user's aggregated interactions (zaps) with an item
|
||||||
@ -579,7 +577,6 @@ model PollVote {
|
|||||||
|
|
||||||
@@index([pollOptionId], map: "PollVote.pollOptionId_index")
|
@@index([pollOptionId], map: "PollVote.pollOptionId_index")
|
||||||
@@index([invoiceId])
|
@@index([invoiceId])
|
||||||
@@index([invoiceActionState])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model PollBlindVote {
|
model PollBlindVote {
|
||||||
@ -596,7 +593,6 @@ model PollBlindVote {
|
|||||||
|
|
||||||
@@unique([itemId, userId], map: "PollBlindVote.itemId_userId_unique")
|
@@unique([itemId, userId], map: "PollBlindVote.itemId_userId_unique")
|
||||||
@@index([userId], map: "PollBlindVote.userId_index")
|
@@index([userId], map: "PollBlindVote.userId_index")
|
||||||
@@index([invoiceActionState])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BillingType {
|
enum BillingType {
|
||||||
@ -735,7 +731,6 @@ model ItemAct {
|
|||||||
@@index([itemId], map: "Vote.itemId_index")
|
@@index([itemId], map: "Vote.itemId_index")
|
||||||
@@index([userId], map: "Vote.userId_index")
|
@@index([userId], map: "Vote.userId_index")
|
||||||
@@index([invoiceId])
|
@@index([invoiceId])
|
||||||
@@index([invoiceActionState])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Mention {
|
model Mention {
|
||||||
@ -826,8 +821,6 @@ model Invoice {
|
|||||||
@@index([createdAt], map: "Invoice.created_at_index")
|
@@index([createdAt], map: "Invoice.created_at_index")
|
||||||
@@index([userId], map: "Invoice.userId_index")
|
@@index([userId], map: "Invoice.userId_index")
|
||||||
@@index([confirmedIndex], map: "Invoice.confirmedIndex_index")
|
@@index([confirmedIndex], map: "Invoice.confirmedIndex_index")
|
||||||
@@index([actionState])
|
|
||||||
@@index([actionType])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Withdrawl {
|
model Withdrawl {
|
||||||
@ -1018,8 +1011,6 @@ enum EarnType {
|
|||||||
COMMENT
|
COMMENT
|
||||||
TIP_COMMENT
|
TIP_COMMENT
|
||||||
TIP_POST
|
TIP_POST
|
||||||
FOREVER_REFERRAL
|
|
||||||
ONE_DAY_REFERRAL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SubActType {
|
enum SubActType {
|
||||||
|
2
sndev
2
sndev
@ -3,7 +3,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
docker__compose() {
|
docker__compose() {
|
||||||
if [ ! -x "$(command -v docker)" ]; then
|
if [ ! -x "$(command -v docker-compose)" ]; then
|
||||||
echo "docker compose is not installed"
|
echo "docker compose is not installed"
|
||||||
echo "installation instructions are here: https://docs.docker.com/desktop/"
|
echo "installation instructions are here: https://docs.docker.com/desktop/"
|
||||||
exit 0
|
exit 0
|
||||||
|
@ -151,8 +151,6 @@ $zindex-sticky: 900;
|
|||||||
--theme-quoteBar: rgb(206, 208, 212);
|
--theme-quoteBar: rgb(206, 208, 212);
|
||||||
--theme-linkHover: #004a72;
|
--theme-linkHover: #004a72;
|
||||||
--theme-linkVisited: #53758;
|
--theme-linkVisited: #53758;
|
||||||
--theme-note-reply: rgba(0, 0, 0, 0.04);
|
|
||||||
--theme-note-fresh: rgba(0, 124, 190, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme=dark] {
|
[data-bs-theme=dark] {
|
||||||
@ -177,8 +175,6 @@ $zindex-sticky: 900;
|
|||||||
--theme-quoteColor: rgb(141, 144, 150);
|
--theme-quoteColor: rgb(141, 144, 150);
|
||||||
--theme-linkHover: #007cbe;
|
--theme-linkHover: #007cbe;
|
||||||
--theme-linkVisited: #56798E;
|
--theme-linkVisited: #56798E;
|
||||||
--theme-note-reply: rgba(255, 255, 255, 0.05);
|
|
||||||
--theme-note-fresh: rgba(0, 124, 190, 0.75);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@import '../node_modules/bootstrap/scss/bootstrap.scss';
|
@import '../node_modules/bootstrap/scss/bootstrap.scss';
|
||||||
|
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M14.1213 10.4792C13.7308 10.0886 13.0976 10.0886 12.7071 10.4792L12 11.1863C11.2189 11.9673 9.95259 11.9673 9.17154 11.1863C8.39049 10.4052 8.39049 9.13888 9.17154 8.35783L14.8022 2.72568C16.9061 2.24973 19.2008 2.83075 20.8388 4.46875C23.2582 6.88811 23.3716 10.7402 21.1792 13.2939L19.071 15.4289L14.1213 10.4792ZM3.16113 4.46875C5.33452 2.29536 8.66411 1.98283 11.17 3.53116L7.75732 6.94362C6.19523 8.50572 6.19523 11.0384 7.75732 12.6005C9.27209 14.1152 11.6995 14.1611 13.2695 12.7382L13.4142 12.6005L17.6568 16.8431L13.4142 21.0858C12.6331 21.8668 11.3668 21.8668 10.5858 21.0858L3.16113 13.6611C0.622722 11.1227 0.622722 7.00715 3.16113 4.46875Z"></path></svg>
|
|
Before Width: | Height: | Size: 756 B |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M14 14.252V22H4C4 17.5817 7.58172 14 12 14C12.6906 14 13.3608 14.0875 14 14.252ZM12 13C8.685 13 6 10.315 6 7C6 3.685 8.685 1 12 1C15.315 1 18 3.685 18 7C18 10.315 15.315 13 12 13ZM18 17V14H20V17H23V19H20V22H18V19H15V17H18Z"></path></svg>
|
|
Before Width: | Height: | Size: 326 B |
118
worker/earn.js
118
worker/earn.js
@ -1,3 +1,4 @@
|
|||||||
|
import serialize from '@/api/resolvers/serial.js'
|
||||||
import { notifyEarner } from '@/lib/webPush.js'
|
import { notifyEarner } from '@/lib/webPush.js'
|
||||||
import { PrismaClient } from '@prisma/client'
|
import { PrismaClient } from '@prisma/client'
|
||||||
import { proportions } from '@/lib/madness.js'
|
import { proportions } from '@/lib/madness.js'
|
||||||
@ -19,8 +20,7 @@ export async function earn ({ name }) {
|
|||||||
|
|
||||||
// XXX primsa will return a Decimal (https://mikemcl.github.io/decimal.js)
|
// XXX primsa will return a Decimal (https://mikemcl.github.io/decimal.js)
|
||||||
// because sum of a BIGINT returns a NUMERIC type (https://www.postgresql.org/docs/13/functions-aggregate.html)
|
// because sum of a BIGINT returns a NUMERIC type (https://www.postgresql.org/docs/13/functions-aggregate.html)
|
||||||
// and Decimal is what prisma maps it to
|
// and Decimal is what prisma maps it to https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access#raw-query-type-mapping
|
||||||
// https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access#raw-query-type-mapping
|
|
||||||
// so check it before coercing to Number
|
// so check it before coercing to Number
|
||||||
if (!sumDecimal || sumDecimal.lessThanOrEqualTo(0)) {
|
if (!sumDecimal || sumDecimal.lessThanOrEqualTo(0)) {
|
||||||
console.log('done', name, 'no sats to award today')
|
console.log('done', name, 'no sats to award today')
|
||||||
@ -48,90 +48,55 @@ export async function earn ({ name }) {
|
|||||||
- how early they upvoted it
|
- how early they upvoted it
|
||||||
- how the post/comment scored
|
- how the post/comment scored
|
||||||
|
|
||||||
Now: 80% of earnings go to top 100 stackers by value, and 10% each to their forever and one day referrers
|
Now: 100% of earnings go to top 33% of comments/posts and their upvoters for month
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// get earners { userId, id, type, rank, proportion, foreverReferrerId, oneDayReferrerId }
|
// get earners { userId, id, type, rank, proportion }
|
||||||
const earners = await models.$queryRaw`
|
const earners = await models.$queryRaw`
|
||||||
WITH earners AS (
|
SELECT id AS "userId", proportion, ROW_NUMBER() OVER (ORDER BY proportion DESC) as rank
|
||||||
SELECT users.id AS "userId", users."referrerId" AS "foreverReferrerId",
|
FROM user_values(date_trunc('day', now() AT TIME ZONE 'America/Chicago' - interval '1 day'), date_trunc('day', now() AT TIME ZONE 'America/Chicago' - interval '1 day'), '1 day'::INTERVAL, 'day')
|
||||||
proportion, (ROW_NUMBER() OVER (ORDER BY proportion DESC))::INTEGER AS rank
|
WHERE NOT (id = ANY (${SN_NO_REWARDS_IDS}))
|
||||||
FROM user_values(
|
ORDER BY proportion DESC
|
||||||
date_trunc('day', now() AT TIME ZONE 'America/Chicago' - interval '1 day'),
|
LIMIT 100`
|
||||||
date_trunc('day', now() AT TIME ZONE 'America/Chicago' - interval '1 day'),
|
|
||||||
'1 day'::INTERVAL,
|
|
||||||
'day') uv
|
|
||||||
JOIN users ON users.id = uv.id
|
|
||||||
WHERE NOT (users.id = ANY (${SN_NO_REWARDS_IDS}))
|
|
||||||
ORDER BY proportion DESC
|
|
||||||
LIMIT 100
|
|
||||||
)
|
|
||||||
SELECT earners.*,
|
|
||||||
COALESCE(
|
|
||||||
mode() WITHIN GROUP (ORDER BY "OneDayReferral"."referrerId"),
|
|
||||||
earners."foreverReferrerId") AS "oneDayReferrerId"
|
|
||||||
FROM earners
|
|
||||||
LEFT JOIN "OneDayReferral" ON "OneDayReferral"."refereeId" = earners."userId"
|
|
||||||
WHERE "OneDayReferral".created_at >= date_trunc('day', now() AT TIME ZONE 'America/Chicago' - interval '1 day')
|
|
||||||
GROUP BY earners."userId", earners."foreverReferrerId", earners.proportion, earners.rank
|
|
||||||
ORDER BY rank ASC`
|
|
||||||
|
|
||||||
// in order to group earnings for users we use the same createdAt time for
|
// in order to group earnings for users we use the same createdAt time for
|
||||||
// all earnings
|
// all earnings
|
||||||
const createdAt = new Date(new Date().getTime())
|
const now = new Date(new Date().getTime())
|
||||||
// stmts is an array of prisma promises we'll call after the loop
|
|
||||||
const stmts = []
|
|
||||||
|
|
||||||
// this is just a sanity check because it seems like a good idea
|
// this is just a sanity check because it seems like a good idea
|
||||||
let total = 0
|
let total = 0
|
||||||
|
|
||||||
const notifications = {}
|
const notifications = {}
|
||||||
for (const [i, earner] of earners.entries()) {
|
for (const [i, earner] of earners.entries()) {
|
||||||
const foreverReferrerEarnings = Math.floor(parseFloat(earner.proportion * sum * 0.1)) // 10% of earnings
|
const earnings = Math.floor(parseFloat(proportions[i] * sum))
|
||||||
let oneDayReferrerEarnings = Math.floor(parseFloat(earner.proportion * sum * 0.1)) // 10% of earnings
|
total += earnings
|
||||||
const earnerEarnings = Math.floor(parseFloat(proportions[i] * sum)) - foreverReferrerEarnings - oneDayReferrerEarnings
|
|
||||||
|
|
||||||
total += earnerEarnings + foreverReferrerEarnings + oneDayReferrerEarnings
|
|
||||||
if (total > sum) {
|
if (total > sum) {
|
||||||
console.log(name, 'total exceeds sum', total, '>', sum)
|
console.log(name, 'total exceeds sum', total, '>', sum)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log('stacker', earner.userId, 'earned', earnings, 'proportion', earner.proportion, 'rank', earner.rank, 'type', earner.type)
|
||||||
'stacker', earner.userId,
|
|
||||||
'earned', earnerEarnings,
|
|
||||||
'proportion', earner.proportion,
|
|
||||||
'rank', earner.rank,
|
|
||||||
'type', earner.type,
|
|
||||||
'foreverReferrer', earner.foreverReferrerId,
|
|
||||||
'foreverReferrerEarnings', foreverReferrerEarnings,
|
|
||||||
'oneDayReferrer', earner.oneDayReferrerId,
|
|
||||||
'oneDayReferrerEarnings', oneDayReferrerEarnings)
|
|
||||||
|
|
||||||
if (earnerEarnings > 0) {
|
if (earnings > 0) {
|
||||||
stmts.push(...earnStmts({
|
await serialize(
|
||||||
msats: earnerEarnings,
|
models.$executeRaw`SELECT earn(${earner.userId}::INTEGER, ${earnings},
|
||||||
userId: earner.userId,
|
${now}::timestamp without time zone, ${earner.type}::"EarnType", ${earner.id}::INTEGER, ${earner.rank}::INTEGER)`,
|
||||||
createdAt,
|
{ models }
|
||||||
type: earner.type,
|
)
|
||||||
rank: earner.rank
|
|
||||||
}, { models }))
|
|
||||||
|
|
||||||
const userN = notifications[earner.userId] || {}
|
const userN = notifications[earner.userId] || {}
|
||||||
|
|
||||||
// sum total
|
// sum total
|
||||||
const prevMsats = userN.msats || 0
|
const prevMsats = userN.msats || 0
|
||||||
const msats = earnerEarnings + prevMsats
|
const msats = earnings + prevMsats
|
||||||
|
|
||||||
// sum total per earn type (POST, COMMENT, TIP_COMMENT, TIP_POST)
|
// sum total per earn type (POST, COMMENT, TIP_COMMENT, TIP_POST)
|
||||||
const prevEarnTypeMsats = userN[earner.type]?.msats || 0
|
const prevEarnTypeMsats = userN[earner.type]?.msats || 0
|
||||||
const earnTypeMsats = earnerEarnings + prevEarnTypeMsats
|
const earnTypeMsats = earnings + prevEarnTypeMsats
|
||||||
|
|
||||||
// best (=lowest) rank per earn type
|
// best (=lowest) rank per earn type
|
||||||
const prevEarnTypeBestRank = userN[earner.type]?.bestRank
|
const prevEarnTypeBestRank = userN[earner.type]?.bestRank
|
||||||
const earnTypeBestRank = prevEarnTypeBestRank
|
const earnTypeBestRank = prevEarnTypeBestRank ? Math.min(prevEarnTypeBestRank, Number(earner.rank)) : Number(earner.rank)
|
||||||
? Math.min(prevEarnTypeBestRank, Number(earner.rank))
|
|
||||||
: Number(earner.rank)
|
|
||||||
|
|
||||||
notifications[earner.userId] = {
|
notifications[earner.userId] = {
|
||||||
...userN,
|
...userN,
|
||||||
@ -139,34 +104,8 @@ export async function earn ({ name }) {
|
|||||||
[earner.type]: { msats: earnTypeMsats, bestRank: earnTypeBestRank }
|
[earner.type]: { msats: earnTypeMsats, bestRank: earnTypeBestRank }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (earner.foreverReferrerId && foreverReferrerEarnings > 0) {
|
|
||||||
stmts.push(...earnStmts({
|
|
||||||
msats: foreverReferrerEarnings,
|
|
||||||
userId: earner.foreverReferrerId,
|
|
||||||
createdAt,
|
|
||||||
type: 'FOREVER_REFERRAL',
|
|
||||||
rank: earner.rank
|
|
||||||
}, { models }))
|
|
||||||
} else if (earner.oneDayReferrerId) {
|
|
||||||
// if the person doesn't have a forever referrer yet, they give double to their one day referrer
|
|
||||||
oneDayReferrerEarnings += foreverReferrerEarnings
|
|
||||||
}
|
|
||||||
|
|
||||||
if (earner.oneDayReferrerId && oneDayReferrerEarnings > 0) {
|
|
||||||
stmts.push(...earnStmts({
|
|
||||||
msats: oneDayReferrerEarnings,
|
|
||||||
userId: earner.oneDayReferrerId,
|
|
||||||
createdAt,
|
|
||||||
type: 'ONE_DAY_REFERRAL',
|
|
||||||
rank: earner.rank
|
|
||||||
}, { models }))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// execute all the transactions
|
|
||||||
await models.$transaction(stmts)
|
|
||||||
|
|
||||||
Promise.allSettled(
|
Promise.allSettled(
|
||||||
Object.entries(notifications).map(([userId, earnings]) => notifyEarner(parseInt(userId, 10), earnings))
|
Object.entries(notifications).map(([userId, earnings]) => notifyEarner(parseInt(userId, 10), earnings))
|
||||||
).catch(console.error)
|
).catch(console.error)
|
||||||
@ -174,16 +113,3 @@ export async function earn ({ name }) {
|
|||||||
models.$disconnect().catch(console.error)
|
models.$disconnect().catch(console.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function earnStmts (data, { models }) {
|
|
||||||
const { msats, userId } = data
|
|
||||||
return [
|
|
||||||
models.earn.create({ data }),
|
|
||||||
models.user.update({
|
|
||||||
where: { id: userId },
|
|
||||||
data: {
|
|
||||||
msats: { increment: msats },
|
|
||||||
stackedMsats: { increment: msats }
|
|
||||||
}
|
|
||||||
})]
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user