Custom date selector for more pages (#567)

* add custom range option to top items page

* add custom range option to profile page

* add date filter option to chart pages

* cleanup

* fix x-axis date labels

* date picker improvements

* enhancements to custom date selection

* remove unneeded condition

---------

Co-authored-by: rleed <rleed1@pm.me>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
rleed 2023-11-08 21:15:36 -03:00 committed by GitHub
parent 8f590425dc
commit 3a56782572
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 609 additions and 465 deletions

View File

@ -1,3 +1,6 @@
import { timeUnitForRange } from '../../lib/time'
import { whenRange } from './item'
const PLACEHOLDERS_NUM = 616
export function interval (when) {
@ -15,27 +18,13 @@ export function interval (when) {
}
}
export function timeUnit (when) {
switch (when) {
case 'week':
case 'month':
return 'day'
case 'year':
case 'forever':
return 'month'
default:
return 'hour'
}
}
export function withClause (when) {
const ival = interval(when)
const unit = timeUnit(when)
export function withClause (range) {
const unit = timeUnitForRange(range)
return `
WITH range_values AS (
SELECT date_trunc('${unit}', ${ival ? "now_utc() - interval '" + ival + "'" : "'2021-06-07'::timestamp"}) as minval,
date_trunc('${unit}', now_utc()) as maxval),
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
@ -43,54 +32,50 @@ export function withClause (when) {
`
}
// HACKY AF this is a performance enhancement that allows us to use the created_at indices on tables
export function intervalClause (when, table, and) {
const unit = timeUnit(when)
if (when === 'forever') {
return and ? '' : 'TRUE'
}
export function intervalClause (range, table) {
const unit = timeUnitForRange(range)
return `"${table}".created_at >= date_trunc('${unit}', now_utc() - interval '${interval(when)}') ${and ? 'AND' : ''} `
return `"${table}".created_at >= date_trunc('${unit}', timezone('America/Chicago', $1)) AND "${table}".created_at <= date_trunc('${unit}', timezone('America/Chicago', $2)) `
}
export function viewIntervalClause (when, view, and) {
if (when === 'forever') {
return and ? '' : 'TRUE'
}
return `"${view}".day >= date_trunc('day', timezone('America/Chicago', now() - interval '${interval(when)}')) ${and ? 'AND' : ''} `
export function viewIntervalClause (range, view) {
return `"${view}".day >= date_trunc('day', timezone('America/Chicago', $1)) AND "${view}".day <= date_trunc('day', timezone('America/Chicago', $2)) `
}
export default {
Query: {
registrationGrowth: async (parent, { when }, { models }) => {
registrationGrowth: async (parent, { when, from, to }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'referrals', 'value', sum(referrals)),
json_build_object('name', 'organic', 'value', sum(organic))
) AS data
FROM reg_growth_days
WHERE ${viewIntervalClause(when, 'reg_growth_days', false)}
WHERE ${viewIntervalClause(range, 'reg_growth_days')}
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(when)}
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'referrals', 'value', count("referrerId")),
json_build_object('name', 'organic', 'value', count(users.id) FILTER(WHERE id > ${PLACEHOLDERS_NUM}) - count("inviteId"))
) AS data
FROM times
LEFT JOIN users ON ${intervalClause(when, 'users', true)} time = date_trunc('${timeUnit(when)}', created_at)
LEFT JOIN users ON ${intervalClause(range, 'users')} AND time = date_trunc('${timeUnitForRange(range)}', created_at)
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
},
spenderGrowth: async (parent, { when }, { models }) => {
spenderGrowth: async (parent, { when, to, from }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'any', 'value', floor(avg("any"))),
json_build_object('name', 'jobs', 'value', floor(avg(jobs))),
json_build_object('name', 'boost', 'value', floor(avg(boost))),
@ -99,13 +84,13 @@ export default {
json_build_object('name', 'donation', 'value', floor(avg(donations)))
) AS data
FROM spender_growth_days
WHERE ${viewIntervalClause(when, 'spender_growth_days', false)}
WHERE ${viewIntervalClause(range, 'spender_growth_days')}
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(when)}
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'any', 'value', count(DISTINCT "userId")),
json_build_object('name', 'jobs', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'STREAM')),
@ -118,31 +103,33 @@ export default {
LEFT JOIN
((SELECT "ItemAct".created_at, "userId", act::text as act
FROM "ItemAct"
WHERE ${intervalClause(when, 'ItemAct', false)})
WHERE ${intervalClause(range, 'ItemAct')})
UNION ALL
(SELECT created_at, "userId", 'DONATION' as act
FROM "Donation"
WHERE ${intervalClause(when, 'Donation', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
WHERE ${intervalClause(range, 'Donation')})) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at)
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
},
itemGrowth: async (parent, { when }, { models }) => {
itemGrowth: async (parent, { when, to, from }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'posts', 'value', sum(posts)),
json_build_object('name', 'comments', 'value', sum(comments)),
json_build_object('name', 'jobs', 'value', sum(jobs)),
json_build_object('name', 'comments/posts', 'value', ROUND(sum(comments)/GREATEST(sum(posts), 1), 2))
) AS data
FROM item_growth_days
WHERE ${viewIntervalClause(when, 'item_growth_days', false)}
WHERE ${viewIntervalClause(range, 'item_growth_days')}
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(when)}
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'comments', 'value', count("parentId")),
json_build_object('name', 'jobs', 'value', count("subName") FILTER (WHERE "subName" = 'jobs')),
@ -150,14 +137,16 @@ export default {
json_build_object('name', 'comments/posts', 'value', ROUND(count("parentId")/GREATEST(count("Item".id)-count("parentId"), 1), 2))
) AS data
FROM times
LEFT JOIN "Item" ON ${intervalClause(when, 'Item', true)} time = date_trunc('${timeUnit(when)}', created_at)
LEFT JOIN "Item" ON ${intervalClause(range, 'Item')} AND time = date_trunc('${timeUnitForRange(range)}', created_at)
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
},
spendingGrowth: async (parent, { when }, { models }) => {
spendingGrowth: async (parent, { when, to, from }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'jobs', 'value', sum(jobs)),
json_build_object('name', 'boost', 'value', sum(boost)),
json_build_object('name', 'fees', 'value', sum(fees)),
@ -165,13 +154,13 @@ export default {
json_build_object('name', 'donations', 'value', sum(donations))
) AS data
FROM spending_growth_days
WHERE ${viewIntervalClause(when, 'spending_growth_days', false)}
WHERE ${viewIntervalClause(range, 'spending_growth_days')}
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(when)}
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'jobs', 'value', coalesce(floor(sum(CASE WHEN act = 'STREAM' THEN msats ELSE 0 END)/1000),0)),
json_build_object('name', 'boost', 'value', coalesce(floor(sum(CASE WHEN act = 'BOOST' THEN msats ELSE 0 END)/1000),0)),
@ -183,18 +172,20 @@ export default {
LEFT JOIN
((SELECT "ItemAct".created_at, msats, act::text as act
FROM "ItemAct"
WHERE ${intervalClause(when, 'ItemAct', false)})
WHERE ${intervalClause(range, 'ItemAct')})
UNION ALL
(SELECT created_at, sats * 1000 as msats, 'DONATION' as act
FROM "Donation"
WHERE ${intervalClause(when, 'Donation', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
WHERE ${intervalClause(range, 'Donation')})) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at)
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
},
stackerGrowth: async (parent, { when }, { models }) => {
stackerGrowth: async (parent, { when, to, from }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'any', 'value', floor(avg("any"))),
json_build_object('name', 'posts', 'value', floor(avg(posts))),
json_build_object('name', 'comments', 'value', floor(floor(avg(comments)))),
@ -202,13 +193,13 @@ export default {
json_build_object('name', 'referrals', 'value', floor(avg(referrals)))
) AS data
FROM stackers_growth_days
WHERE ${viewIntervalClause(when, 'stackers_growth_days', false)}
WHERE ${viewIntervalClause(range, 'stackers_growth_days')}
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(when)}
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'any', 'value', count(distinct user_id)),
json_build_object('name', 'posts', 'value', count(distinct user_id) FILTER (WHERE type = 'POST')),
@ -221,35 +212,37 @@ export default {
((SELECT "ItemAct".created_at, "Item"."userId" as user_id, CASE WHEN "Item"."parentId" IS NULL THEN 'POST' ELSE 'COMMENT' END as type
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE ${intervalClause(when, 'ItemAct', true)} "ItemAct".act = 'TIP')
WHERE ${intervalClause(range, 'ItemAct')} AND "ItemAct".act = 'TIP')
UNION ALL
(SELECT created_at, "userId" as user_id, 'EARN' as type
FROM "Earn"
WHERE ${intervalClause(when, 'Earn', false)})
WHERE ${intervalClause(range, 'Earn')})
UNION ALL
(SELECT created_at, "referrerId" as user_id, 'REFERRAL' as type
FROM "ReferralAct"
WHERE ${intervalClause(when, 'ReferralAct', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
WHERE ${intervalClause(range, 'ReferralAct')})) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at)
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
},
stackingGrowth: async (parent, { when }, { models }) => {
stackingGrowth: async (parent, { when, to, from }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'rewards', 'value', sum(rewards)),
json_build_object('name', 'posts', 'value', sum(posts)),
json_build_object('name', 'comments', 'value', sum(comments)),
json_build_object('name', 'referrals', 'value', sum(referrals))
) AS data
FROM stacking_growth_days
WHERE ${viewIntervalClause(when, 'stacking_growth_days', false)}
WHERE ${viewIntervalClause(range, 'stacking_growth_days')}
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(when)}
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'rewards', 'value', coalesce(floor(sum(airdrop)/1000),0)),
json_build_object('name', 'posts', 'value', coalesce(floor(sum(post)/1000),0)),
@ -264,17 +257,17 @@ export default {
0 as referral
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE ${intervalClause(when, 'ItemAct', true)} "ItemAct".act = 'TIP')
WHERE ${intervalClause(range, 'ItemAct')} AND "ItemAct".act = 'TIP')
UNION ALL
(SELECT created_at, 0 as airdrop, 0 as post, 0 as comment, msats as referral
FROM "ReferralAct"
WHERE ${intervalClause(when, 'ReferralAct', false)})
WHERE ${intervalClause(range, 'ReferralAct')})
UNION ALL
(SELECT created_at, msats as airdrop, 0 as post, 0 as comment, 0 as referral
FROM "Earn"
WHERE ${intervalClause(when, 'Earn', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
WHERE ${intervalClause(range, 'Earn')})) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at)
GROUP BY time
ORDER BY time ASC`)
ORDER BY time ASC`, ...range)
}
}
}

View File

@ -18,7 +18,7 @@ import { advSchema, amountSchema, bountySchema, commentSchema, discussionSchema,
import { sendUserNotification } from '../webPush'
import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand } from '../../lib/item'
import { notifyItemParents, notifyUserSubscribers, notifyZapped } from '../../lib/push-notifications'
import { datePivot } from '../../lib/time'
import { datePivot, dayMonthYearToDate, whenToFrom } from '../../lib/time'
import { imageFeesInfo, uploadIdsFromText } from './image'
export async function commentFilterClause (me, models) {
@ -195,26 +195,17 @@ export const whereClause = (...clauses) => {
return clause ? ` WHERE ${clause} ` : ''
}
function whenClause (when, type) {
let interval = `"${type === 'bookmarks' ? 'Bookmark' : 'Item'}".created_at >= $1 - INTERVAL `
function whenClause (when, table) {
return `"${table}".created_at <= $2 and "${table}".created_at >= $1`
}
export function whenRange (when, from, to = new Date()) {
switch (when) {
case 'forever':
interval = ''
break
case 'week':
interval += "'7 days'"
break
case 'month':
interval += "'1 month'"
break
case 'year':
interval += "'1 year'"
break
case 'custom':
return [new Date(from), new Date(to)]
default:
interval += "'1 day'"
break
return [dayMonthYearToDate(whenToFrom(when)), new Date(to)]
}
return interval
}
const activeOrMine = (me) => {
@ -309,7 +300,7 @@ export default {
return count
},
items: async (parent, { sub, sort, type, cursor, name, when, by, limit = LIMIT }, { me, models }) => {
items: async (parent, { sub, sort, type, cursor, name, when, from, to, by, limit = LIMIT }, { me, models }) => {
const decodedCursor = decodeCursor(cursor)
let items, user, pins, subFull, table
@ -354,18 +345,16 @@ export default {
${selectClause(type)}
${relationClause(type)}
${whereClause(
`"${table}"."userId" = $2`,
`"${table}".created_at <= $1`,
subClause(sub, 5, subClauseTable(type)),
`"${table}"."userId" = $3`,
activeOrMine(me),
await filterClause(me, models, type),
typeClause(type),
whenClause(when || 'forever', type))}
whenClause(when || 'forever', table))}
${orderByClause(by, me, models, type)}
OFFSET $3
LIMIT $4`,
OFFSET $4
LIMIT $5`,
orderBy: orderByClause(by, me, models, type)
}, decodedCursor.time, user.id, decodedCursor.offset, limit, ...subArr)
}, ...whenRange(when, from, to || decodedCursor.time), user.id, decodedCursor.offset, limit)
break
case 'recent':
items = await itemQueryWithMeta({
@ -399,19 +388,18 @@ export default {
${relationClause(type)}
${joinZapRankPersonalView(me, models)}
${whereClause(
'"Item".created_at <= $1',
'"Item"."pinId" IS NULL',
'"Item"."deletedAt" IS NULL',
subClause(sub, 4, subClauseTable(type)),
subClause(sub, 5, subClauseTable(type)),
typeClause(type),
whenClause(when, type),
whenClause(when, 'Item'),
await filterClause(me, models, type),
muteClause(me))}
ORDER BY rank DESC
OFFSET $2
LIMIT $3`,
OFFSET $3
LIMIT $4`,
orderBy: 'ORDER BY rank DESC'
}, decodedCursor.time, decodedCursor.offset, limit, ...subArr)
}, ...whenRange(when, from, to || decodedCursor.time), decodedCursor.offset, limit, ...subArr)
} else {
items = await itemQueryWithMeta({
me,
@ -420,19 +408,18 @@ export default {
${selectClause(type)}
${relationClause(type)}
${whereClause(
'"Item".created_at <= $1',
'"Item"."pinId" IS NULL',
'"Item"."deletedAt" IS NULL',
subClause(sub, 4, subClauseTable(type)),
subClause(sub, 5, subClauseTable(type)),
typeClause(type),
whenClause(when, type),
whenClause(when, 'Item'),
await filterClause(me, models, type),
muteClause(me))}
${orderByClause(by || 'zaprank', me, models, type)}
OFFSET $2
LIMIT $3`,
OFFSET $3
LIMIT $4`,
orderBy: orderByClause(by || 'zaprank', me, models, type)
}, decodedCursor.time, decodedCursor.offset, limit, ...subArr)
}, ...whenRange(when, from, to || decodedCursor.time), decodedCursor.offset, limit, ...subArr)
}
break
default:
@ -1052,7 +1039,7 @@ export const createMentions = async (item, models) => {
})
}
} catch (e) {
console.log('mention failure', e)
console.error('mention failure', e)
}
}

View File

@ -1,29 +1,33 @@
import { GraphQLError } from 'graphql'
import { withClause, intervalClause, timeUnit } from './growth'
import { withClause, intervalClause } from './growth'
import { whenRange } from './item'
import { timeUnitForRange } from '../../lib/time'
export default {
Query: {
referrals: async (parent, { when }, { models, me }) => {
referrals: async (parent, { when, from, to }, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
}
const range = whenRange(when, from, to)
const [{ totalSats }] = await models.$queryRawUnsafe(`
SELECT COALESCE(FLOOR(sum(msats) / 1000), 0) as "totalSats"
FROM "ReferralAct"
WHERE ${intervalClause(when, 'ReferralAct', true)}
"ReferralAct"."referrerId" = $1
`, Number(me.id))
WHERE ${intervalClause(range, 'ReferralAct')}
AND "ReferralAct"."referrerId" = $3
`, ...range, Number(me.id))
const [{ totalReferrals }] = await models.$queryRawUnsafe(`
SELECT count(*)::INTEGER as "totalReferrals"
FROM users
WHERE ${intervalClause(when, 'users', true)}
"referrerId" = $1
`, Number(me.id))
WHERE ${intervalClause(range, 'users')}
AND "referrerId" = $3
`, ...range, Number(me.id))
const stats = await models.$queryRawUnsafe(
`${withClause(when)}
`${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)))
@ -33,15 +37,15 @@ export default {
((SELECT "ReferralAct".created_at, "ReferralAct".msats / 1000.0 as msats, "ItemAct".act::text as act
FROM "ReferralAct"
JOIN "ItemAct" ON "ItemAct".id = "ReferralAct"."itemActId"
WHERE ${intervalClause(when, 'ReferralAct', true)}
"ReferralAct"."referrerId" = $1)
WHERE ${intervalClause(range, 'ReferralAct')}
AND "ReferralAct"."referrerId" = $3)
UNION ALL
(SELECT created_at, 0.0 as sats, 'REFERREE' as act
FROM users
WHERE ${intervalClause(when, 'users', true)}
"referrerId" = $1)) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
WHERE ${intervalClause(range, 'users')}
AND "referrerId" = $3)) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at)
GROUP BY time
ORDER BY time ASC`, Number(me.id))
ORDER BY time ASC`, ...range, Number(me.id))
return {
totalSats,

View File

@ -1,5 +1,6 @@
import { ITEM_FILTER_THRESHOLD } from '../../lib/constants'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { whenToFrom } from '../../lib/time'
import { getItem } from './item'
const STOP_WORDS = ['a', 'an', 'and', 'are', 'as', 'at', 'be', 'but',
@ -124,20 +125,25 @@ export default {
switch (sort) {
case 'recent':
sortArr.push({ createdAt: 'desc' })
sortArr.push('_score')
break
case 'comments':
sortArr.push({ ncomments: 'desc' })
sortArr.push('_score')
break
case 'sats':
sortArr.push({ sats: 'desc' })
sortArr.push('_score')
break
case 'match':
sortArr.push('_score')
sortArr.push({ wvotes: 'desc' })
break
default:
sortArr.push({ wvotes: 'desc' })
sortArr.push('_score')
break
}
sortArr.push('_score')
if (query.length) {
whatArr.push({
@ -181,32 +187,14 @@ export default {
})
}
let whenGte
switch (when) {
case 'day':
whenGte = 'now-1d'
break
case 'week':
whenGte = 'now-7d'
break
case 'month':
whenGte = 'now-30d'
break
case 'year':
whenGte = 'now-365d'
break
default:
break
}
const whenRange = when === 'custom'
? {
gte: new Date(whenFrom),
gte: whenFrom,
lte: new Date(Math.min(new Date(whenTo), decodedCursor.time))
}
: {
lte: decodedCursor.time,
gte: whenGte
gte: whenToFrom(when)
}
try {

View File

@ -4,9 +4,9 @@ import { GraphQLError } from 'graphql'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { msatsToSats } from '../../lib/format'
import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '../../lib/validate'
import { getItem, updateItem, filterClause, createItem, whereClause, muteClause } from './item'
import { datePivot } from '../../lib/time'
import { getItem, updateItem, filterClause, createItem, whereClause, muteClause, whenRange } from './item'
import { ANON_USER_ID, DELETE_USER_ID, RESERVED_MAX_USER_ID } from '../../lib/constants'
import { viewIntervalClause, intervalClause } from './growth'
const contributors = new Set()
@ -22,66 +22,6 @@ const loadContributors = async (set) => {
}
}
export function within (table, within) {
let interval = ' AND "' + table + '".created_at >= $1 - INTERVAL '
switch (within) {
case 'day':
interval += "'1 day'"
break
case 'week':
interval += "'7 days'"
break
case 'month':
interval += "'1 month'"
break
case 'year':
interval += "'1 year'"
break
default:
interval = ''
break
}
return interval
}
export function viewWithin (table, within) {
let interval = ' AND "' + table + '".day >= date_trunc(\'day\', timezone(\'America/Chicago\', $1 at time zone \'UTC\' - interval '
switch (within) {
case 'day':
interval += "'1 day'))"
break
case 'week':
interval += "'7 days'))"
break
case 'month':
interval += "'1 month'))"
break
case 'year':
interval += "'1 year'))"
break
default:
// HACK: we need to use the time parameter otherwise prisma *cries* about it
interval = ' AND users.created_at <= $1'
break
}
return interval
}
export function withinDate (within) {
switch (within) {
case 'day':
return datePivot(new Date(), { days: -1 })
case 'week':
return datePivot(new Date(), { days: -7 })
case 'month':
return datePivot(new Date(), { days: -30 })
case 'year':
return datePivot(new Date(), { days: -365 })
default:
return new Date(0)
}
}
async function authMethods (user, args, { models, me }) {
const accounts = await models.account.findMany({
where: {
@ -149,8 +89,9 @@ export default {
users
}
},
topUsers: async (parent, { cursor, when, by, limit = LIMIT }, { models, me }) => {
topUsers: async (parent, { cursor, when, by, from, to, limit = LIMIT }, { models, me }) => {
const decodedCursor = decodeCursor(cursor)
const range = whenRange(when, from, to || decodeCursor.time)
let users
if (when !== 'day') {
@ -171,13 +112,13 @@ export default {
FROM user_stats_days
JOIN users on users.id = user_stats_days.id
WHERE NOT users."hideFromTopUsers"
${viewWithin('user_stats_days', when)}
AND ${viewIntervalClause(range, 'user_stats_days')}
GROUP BY users.id
ORDER BY ${column} DESC NULLS LAST, users.created_at DESC
)
SELECT * FROM u WHERE ${column} > 0
OFFSET $2
LIMIT ${limit}`, decodedCursor.time, decodedCursor.offset)
OFFSET $3
LIMIT $4`, ...range, decodedCursor.offset, limit)
return {
cursor: users.length === limit ? nextCursorEncoded(decodedCursor, limit) : null,
@ -191,56 +132,53 @@ export default {
FROM
((SELECT "userId", floor(sum("ItemAct".msats)/1000) as sats_spent
FROM "ItemAct"
WHERE "ItemAct".created_at <= $1
${within('ItemAct', when)}
WHERE ${intervalClause(range, 'ItemAct')}
GROUP BY "userId")
UNION ALL
(SELECT "userId", sats as sats_spent
FROM "Donation"
WHERE created_at <= $1
${within('Donation', when)})) spending
WHERE ${intervalClause(range, 'Donation')})) spending
JOIN users on spending."userId" = users.id
AND NOT users."hideFromTopUsers"
GROUP BY users.id, users.name
ORDER BY spent DESC NULLS LAST, users.created_at DESC
OFFSET $2
LIMIT ${limit}`, decodedCursor.time, decodedCursor.offset)
OFFSET $3
LIMIT $4`, ...range, decodedCursor.offset, limit)
} else if (by === 'posts') {
users = await models.$queryRawUnsafe(`
SELECT users.*, count(*)::INTEGER as nposts
FROM users
JOIN "Item" on "Item"."userId" = users.id
WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NULL
WHERE "Item"."parentId" IS NULL
AND NOT users."hideFromTopUsers"
${within('Item', when)}
AND ${viewIntervalClause(range, 'Item')}
GROUP BY users.id
ORDER BY nposts DESC NULLS LAST, users.created_at DESC
OFFSET $2
LIMIT ${limit}`, decodedCursor.time, decodedCursor.offset)
OFFSET $3
LIMIT $4`, ...range, decodedCursor.offset, limit)
} else if (by === 'comments') {
users = await models.$queryRawUnsafe(`
SELECT users.*, count(*)::INTEGER as ncomments
FROM users
JOIN "Item" on "Item"."userId" = users.id
WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NOT NULL
WHERE "Item"."parentId" IS NOT NULL
AND NOT users."hideFromTopUsers"
${within('Item', when)}
AND ${intervalClause(range, 'Item')}
GROUP BY users.id
ORDER BY ncomments DESC NULLS LAST, users.created_at DESC
OFFSET $2
LIMIT ${limit}`, decodedCursor.time, decodedCursor.offset)
OFFSET $3
LIMIT $4`, ...range, decodedCursor.offset, limit)
} else if (by === 'referrals') {
users = await models.$queryRawUnsafe(`
SELECT users.*, count(*)::INTEGER as referrals
FROM users
JOIN "users" referree on users.id = referree."referrerId"
WHERE referree.created_at <= $1
AND NOT users."hideFromTopUsers"
${within('referree', when)}
AND ${intervalClause(range, 'referree')}
GROUP BY users.id
ORDER BY referrals DESC NULLS LAST, users.created_at DESC
OFFSET $2
LIMIT ${limit}`, decodedCursor.time, decodedCursor.offset)
OFFSET $3
LIMIT $4`, ...range, decodedCursor.offset, limit)
} else {
users = await models.$queryRawUnsafe(`
SELECT u.id, u.name, u.streak, u."photoId", u."hideCowboyHat", floor(sum(amount)/1000) as stacked
@ -249,25 +187,27 @@ export default {
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
JOIN users on "Item"."userId" = users.id
WHERE act <> 'BOOST' AND "ItemAct"."userId" <> users.id AND "ItemAct".created_at <= $1
WHERE act <> 'BOOST' AND "ItemAct"."userId" <> users.id
AND NOT users."hideFromTopUsers"
${within('ItemAct', when)})
AND ${intervalClause(range, 'ItemAct')})
UNION ALL
(SELECT users.*, "Earn".msats as amount
FROM "Earn"
JOIN users on users.id = "Earn"."userId"
WHERE "Earn".msats > 0 ${within('Earn', when)}
WHERE "Earn".msats > 0
AND ${intervalClause(range, 'Earn')}
AND NOT users."hideFromTopUsers")
UNION ALL
(SELECT users.*, "ReferralAct".msats as amount
FROM "ReferralAct"
JOIN users on users.id = "ReferralAct"."referrerId"
WHERE "ReferralAct".msats > 0 ${within('ReferralAct', when)}
WHERE "ReferralAct".msats > 0
AND ${intervalClause(range, 'ReferralAct')}
AND NOT users."hideFromTopUsers")) u
GROUP BY u.id, u.name, u.created_at, u."photoId", u.streak, u."hideCowboyHat"
ORDER BY stacked DESC NULLS LAST, created_at DESC
OFFSET $2
LIMIT ${limit}`, decodedCursor.time, decodedCursor.offset)
OFFSET $3
LIMIT $4`, ...range, decodedCursor.offset, limit)
}
return {
@ -696,16 +636,18 @@ export default {
FROM "Streak" WHERE "userId" = ${user.id}`
return max
},
nitems: async (user, { when }, { models }) => {
nitems: async (user, { when, from, to }, { models }) => {
if (typeof user.nitems !== 'undefined') {
return user.nitems
}
const [gte, lte] = whenRange(when, from, to)
return await models.item.count({
where: {
userId: user.id,
createdAt: {
gte: withinDate(when)
gte,
lte
}
}
})
@ -725,51 +667,57 @@ export default {
return !!mute
},
nposts: async (user, { when }, { models }) => {
nposts: async (user, { when, from, to }, { models }) => {
if (typeof user.nposts !== 'undefined') {
return user.nposts
}
const [gte, lte] = whenRange(when, from, to)
return await models.item.count({
where: {
userId: user.id,
parentId: null,
createdAt: {
gte: withinDate(when)
gte,
lte
}
}
})
},
ncomments: async (user, { when }, { models }) => {
ncomments: async (user, { when, from, to }, { models }) => {
if (typeof user.ncomments !== 'undefined') {
return user.ncomments
}
const [gte, lte] = whenRange(when, from, to)
return await models.item.count({
where: {
userId: user.id,
parentId: { not: null },
createdAt: {
gte: withinDate(when)
gte,
lte
}
}
})
},
nbookmarks: async (user, { when }, { models }) => {
nbookmarks: async (user, { when, from, to }, { models }) => {
if (typeof user.nBookmarks !== 'undefined') {
return user.nBookmarks
}
const [gte, lte] = whenRange(when, from, to)
return await models.bookmark.count({
where: {
userId: user.id,
createdAt: {
gte: withinDate(when)
gte,
lte
}
}
})
},
stacked: async (user, { when }, { models }) => {
stacked: async (user, { when, from, to }, { models }) => {
if (typeof user.stacked !== 'undefined') {
return user.stacked
}
@ -778,34 +726,36 @@ export default {
// forever
return (user.stackedMsats && msatsToSats(user.stackedMsats)) || 0
} else if (when === 'day') {
const range = whenRange(when, from, to)
const [{ stacked }] = await models.$queryRawUnsafe(`
SELECT sum(amount) as stacked
FROM
((SELECT coalesce(sum("ItemAct".msats),0) as amount
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE act = 'TIP' AND "ItemAct"."userId" <> $2 AND "Item"."userId" = $2
AND "ItemAct".created_at >= $1)
WHERE act = 'TIP' AND "ItemAct"."userId" <> $3 AND "Item"."userId" = $3
AND ${intervalClause(range, 'ItemAct')})
UNION ALL
(SELECT coalesce(sum("ReferralAct".msats),0) as amount
FROM "ReferralAct"
WHERE "ReferralAct".msats > 0 AND "ReferralAct"."referrerId" = $2
AND "ReferralAct".created_at >= $1)
WHERE "ReferralAct".msats > 0 AND "ReferralAct"."referrerId" = $3
AND ${intervalClause(range, 'ReferralAct')})
UNION ALL
(SELECT coalesce(sum("Earn".msats), 0) as amount
FROM "Earn"
WHERE "Earn".msats > 0 AND "Earn"."userId" = $2
AND "Earn".created_at >= $1)) u`, withinDate(when), Number(user.id))
WHERE "Earn".msats > 0 AND "Earn"."userId" = $3
AND ${intervalClause(range, 'Earn')})) u`, ...range, Number(user.id))
return (stacked && msatsToSats(stacked)) || 0
}
return 0
},
spent: async (user, { when }, { models }) => {
spent: async (user, { when, from, to }, { models }) => {
if (typeof user.spent !== 'undefined') {
return user.spent
}
const [gte, lte] = whenRange(when, from, to)
const { _sum: { msats } } = await models.itemAct.aggregate({
_sum: {
msats: true
@ -813,23 +763,26 @@ export default {
where: {
userId: user.id,
createdAt: {
gte: withinDate(when)
gte,
lte
}
}
})
return (msats && msatsToSats(msats)) || 0
},
referrals: async (user, { when }, { models }) => {
referrals: async (user, { when, from, to }, { models }) => {
if (typeof user.referrals !== 'undefined') {
return user.referrals
}
const [gte, lte] = whenRange(when, from, to)
return await models.user.count({
where: {
referrerId: user.id,
createdAt: {
gte: withinDate(when)
gte,
lte
}
}
})

View File

@ -7,12 +7,12 @@ export default gql`
}
extend type Query {
registrationGrowth(when: String): [TimeData!]!
itemGrowth(when: String): [TimeData!]!
spendingGrowth(when: String): [TimeData!]!
spenderGrowth(when: String): [TimeData!]!
stackingGrowth(when: String): [TimeData!]!
stackerGrowth(when: String): [TimeData!]!
registrationGrowth(when: String, from: String, to: String): [TimeData!]!
itemGrowth(when: String, from: String, to: String): [TimeData!]!
spendingGrowth(when: String, from: String, to: String): [TimeData!]!
spenderGrowth(when: String, from: String, to: String): [TimeData!]!
stackingGrowth(when: String, from: String, to: String): [TimeData!]!
stackerGrowth(when: String, from: String, to: String): [TimeData!]!
}
type TimeData {

View File

@ -2,7 +2,7 @@ import { gql } from 'graphql-tag'
export default gql`
extend type Query {
items(sub: String, sort: String, type: String, cursor: String, name: String, when: String, by: String, limit: Int): Items
items(sub: String, sort: String, type: String, cursor: String, name: String, when: String, from: String, to: String, by: String, limit: Int): Items
item(id: ID!): Item
pageTitleAndUnshorted(url: String!): TitleUnshorted
dupes(url: String!): [Item!]

View File

@ -2,7 +2,7 @@ import { gql } from 'graphql-tag'
export default gql`
extend type Query {
referrals(when: String): Referrals!
referrals(when: String, from: String, to: String): Referrals!
}
type Referrals {

View File

@ -7,7 +7,7 @@ export default gql`
user(name: String!): User
users: [User!]
nameAvailable(name: String!): Boolean!
topUsers(cursor: String, when: String, by: String, limit: Int): Users
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Int): Users
topCowboys(cursor: String): Users
searchUsers(q: String!, limit: Int, similarity: Float): [User!]!
hasNewNotes: Boolean!
@ -61,13 +61,13 @@ export default gql`
id: ID!
createdAt: Date!
name: String
nitems(when: String): Int!
nposts(when: String): Int!
ncomments(when: String): Int!
nbookmarks(when: String): Int!
stacked(when: String): Int!
spent(when: String): Int!
referrals(when: String): Int!
nitems(when: String, from: String, to: String): Int!
nposts(when: String, from: String, to: String): Int!
ncomments(when: String, from: String, to: String): Int!
nbookmarks(when: String, from: String, to: String): Int!
stacked(when: String, from: String, to: String): Int!
spent(when: String, from: String, to: String): Int!
referrals(when: String, from: String, to: String): Int!
freePosts: Int!
freeComments: Int!
hasInvites: Boolean!

View File

@ -14,16 +14,17 @@ import { Cell } from 'recharts/lib/component/Cell'
import { Pie } from 'recharts/lib/polar/Pie'
import { abbrNum } from '../lib/format'
import { useRouter } from 'next/router'
import { timeUnitForRange } from '../lib/time'
const dateFormatter = when => {
const dateFormatter = (when, from, to) => {
const unit = xAxisName(when, from, to)
return timeStr => {
const date = new Date(timeStr)
switch (when) {
switch (unit) {
case 'day':
case 'week':
case 'month':
return `${('0' + (date.getUTCMonth() % 12 + 1)).slice(-2)}/${date.getUTCDate()}`
case 'year':
case 'forever':
case 'month':
return `${('0' + (date.getUTCMonth() % 12 + 1)).slice(-2)}/${String(date.getUTCFullYear()).slice(-2)}`
default:
return `${date.getHours() % 12 || 12}${date.getHours() >= 12 ? 'pm' : 'am'}`
@ -31,16 +32,25 @@ const dateFormatter = when => {
}
}
function xAxisName (when) {
const labelFormatter = (when, from, to) => {
const unit = xAxisName(when, from, to)
const dateFormat = dateFormatter(when, from, to)
return timeStr => `${unit} ${dateFormat(timeStr)}`
}
function xAxisName (when, from, to) {
if (from) {
return timeUnitForRange([from, to])
}
switch (when) {
case 'week':
case 'month':
return 'days'
return 'day'
case 'year':
case 'forever':
return 'months'
return 'month'
default:
return 'hours'
return 'hour'
}
}
@ -72,6 +82,8 @@ export function WhenAreaChart ({ data }) {
data = transformData(data)
// need to grab when
const when = router.query.when
const from = router.query.from
const to = router.query.to
return (
<ResponsiveContainer width='100%' height={300} minWidth={300}>
@ -85,11 +97,11 @@ export function WhenAreaChart ({ data }) {
}}
>
<XAxis
dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
dataKey='time' tickFormatter={dateFormatter(when, from, to)} name={xAxisName(when, from, to)}
tick={{ fill: 'var(--theme-grey)' }}
/>
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
<Tooltip labelFormatter={dateFormatter(when)} 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 />
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
<Area key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
@ -107,6 +119,8 @@ export function WhenLineChart ({ data }) {
data = transformData(data)
// need to grab when
const when = router.query.when
const from = router.query.from
const to = router.query.to
return (
<ResponsiveContainer width='100%' height={300} minWidth={300}>
@ -120,11 +134,11 @@ export function WhenLineChart ({ data }) {
}}
>
<XAxis
dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
dataKey='time' tickFormatter={dateFormatter(when, from, to)} name={xAxisName(when, from, to)}
tick={{ fill: 'var(--theme-grey)' }}
/>
<YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
<Tooltip labelFormatter={dateFormatter(when)} 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 />
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
<Line key={v} type='monotone' dataKey={v} name={v} stroke={COLORS[i]} fill={COLORS[i]} />)}
@ -147,6 +161,8 @@ export function WhenComposedChart ({
data = transformData(data)
// need to grab when
const when = router.query.when
const from = router.query.from
const to = router.query.to
return (
<ResponsiveContainer width='100%' height={300} minWidth={300}>
@ -160,12 +176,12 @@ export function WhenComposedChart ({
}}
>
<XAxis
dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
dataKey='time' tickFormatter={dateFormatter(when, from, to)} name={xAxisName(when, from, to)}
tick={{ fill: 'var(--theme-grey)' }}
/>
<YAxis yAxisId='left' orientation='left' allowDecimals={false} stroke='var(--theme-grey)' tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
<YAxis yAxisId='right' orientation='right' allowDecimals={false} stroke='var(--theme-grey)' tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
<Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--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 />
{barNames?.map((v, i) =>
<Bar yAxisId={barAxis} key={v} type='monotone' dataKey={v} name={v} stroke='var(--bs-info)' fill='var(--bs-info)' />)}

View File

@ -2,7 +2,7 @@ import Button from 'react-bootstrap/Button'
import InputGroup from 'react-bootstrap/InputGroup'
import BootstrapForm from 'react-bootstrap/Form'
import { Formik, Form as FormikForm, useFormikContext, useField, FieldArray } from 'formik'
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import copy from 'clipboard-copy'
import Col from 'react-bootstrap/Col'
import Dropdown from 'react-bootstrap/Dropdown'
@ -26,13 +26,16 @@ import 'react-datepicker/dist/react-datepicker.css'
import { debounce } from './use-debounce-callback'
import { ImageUpload } from './image'
import { AWS_S3_URL_REGEXP } from '../lib/constants'
import { dayMonthYear, dayMonthYearToDate, whenToFrom } from '../lib/time'
export function SubmitButton ({
children, variant, value, onClick, disabled, cost, ...props
}) {
const formik = useFormikContext()
useEffect(() => {
formik?.setFieldValue('cost', cost)
if (cost) {
formik?.setFieldValue('cost', cost)
}
}, [formik?.setFieldValue, formik?.getFieldProps('cost').value, cost])
return (
@ -712,7 +715,8 @@ export function Checkbox ({ children, label, groupClassName, hiddenLabel, extra,
const StorageKeyPrefixContext = createContext()
export function Form ({
initial, schema, onSubmit, children, initialError, validateImmediately, storageKeyPrefix, validateOnChange = true, invoiceable, innerRef, ...props
initial, schema, onSubmit, children, initialError, validateImmediately,
storageKeyPrefix, validateOnChange = true, invoiceable, innerRef, ...props
}) {
const toaster = useToast()
const initialErrorToasted = useRef(false)
@ -754,10 +758,12 @@ export function Form ({
// extract cost from formik fields
// (cost may also be set in a formik field named 'amount')
let cost = values?.cost || values?.amount
// add potential image fees which are set in a different field
// to differentiate between fees (in receipts for example)
cost += (values?.imageFeesInfo?.totalFees || 0)
values.cost = cost
if (cost) {
// add potential image fees which are set in a different field
// to differentiate between fees (in receipts for example)
cost += (values?.imageFeesInfo?.totalFees || 0)
values.cost = cost
}
const options = await onSubmit(values, ...args)
if (!storageKeyPrefix || options?.keepLocalStorage) return
@ -814,7 +820,7 @@ export function Select ({ label, items, groupClassName, onChange, noForm, overri
}}
isInvalid={invalid}
>
{items.map(item => <option key={item}>{item}</option>)}
{items?.map(item => <option key={item}>{item}</option>)}
</BootstrapForm.Select>
<BootstrapForm.Control.Feedback type='invalid'>
{meta.touched && meta.error}
@ -823,28 +829,81 @@ export function Select ({ label, items, groupClassName, onChange, noForm, overri
)
}
export function DatePicker ({ fromName, toName, noForm, onMount, ...props }) {
export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to, className, ...props }) {
const formik = noForm ? null : useFormikContext()
const onChangeHandler = props.onChange
const [,, fromHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: fromName })
const [,, toHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: toName })
const { minDate, maxDate } = props
const [{ innerFrom, innerTo }, setRange] = useState({
innerFrom: from || whenToFrom(when),
innerTo: to || dayMonthYear(new Date())
})
useEffect(() => {
if (onMount) {
const [from, to] = onMount()
const tfrom = from || whenToFrom(when)
const tto = to || dayMonthYear(new Date())
setRange({ innerFrom: tfrom, innerTo: tto })
if (!noForm) {
fromHelpers.setValue(tfrom)
toHelpers.setValue(tto)
}
}, [when, from, to])
const dateFormat = useMemo(() => {
const now = new Date(2013, 11, 31)
let str = now.toLocaleDateString()
str = str.replace('31', 'dd')
str = str.replace('12', 'MM')
str = str.replace('2013', 'yy')
return str
}, [])
const innerOnChange = ([from, to], e) => {
from = dayMonthYear(from)
to = to ? dayMonthYear(to) : undefined
setRange({ innerFrom: from, innerTo: to })
if (!noForm) {
fromHelpers.setValue(from)
toHelpers.setValue(to)
}
}, [])
onChange(formik, [from, to], e)
}
const onChangeRawHandler = (e) => {
// raw user data can be incomplete while typing, so quietly bail on exceptions
try {
const dateStrings = e.target.value.split('-', 2)
const dates = dateStrings.map(s => new Date(s))
let [from, to] = dates
if (from) {
if (minDate) from = new Date(Math.max(from, minDate))
try {
if (maxDate) to = new Date(Math.min(to, maxDate))
// if end date isn't valid, set it to the start date
if (!(to instanceof Date && !isNaN(to)) || to < from) to = from
} catch {
to = from
}
innerOnChange([from, to], e)
}
} catch { }
}
return (
<ReactDatePicker
className={`form-control text-center ${className}`}
selectsRange
maxDate={new Date()}
minDate={new Date('2021-05-01')}
{...props}
onChange={([from, to], e) => {
fromHelpers.setValue(from?.toISOString())
toHelpers.setValue(to?.toISOString())
onChangeHandler(formik, [from, to], e)
}}
selected={dayMonthYearToDate(innerFrom)}
startDate={dayMonthYearToDate(innerFrom)}
endDate={innerTo ? dayMonthYearToDate(innerTo) : undefined}
dateFormat={dateFormat}
onChangeRaw={onChangeRawHandler}
onChange={innerOnChange}
/>
)
}

View File

@ -33,6 +33,8 @@
background-color: var(--theme-inputBg);
border: 1px solid var(--theme-borderColor);
padding: 0rem 0.5rem;
display: flex;
align-items: center;
}
.clearButton:hover, .clearButton:active {

View File

@ -78,9 +78,9 @@ const defaultOnClick = n => {
if (type === 'Earn') {
let href = '/rewards/'
if (n.minSortTime !== n.sortTime) {
href += `${new Date(n.minSortTime).toISOString().slice(0, 10)}/`
href += `${dayMonthYear(new Date(n.minSortTime))}/`
}
href += new Date(n.sortTime).toISOString().slice(0, 10)
href += dayMonthYear(new Date(n.sortTime))
return { href }
}
if (type === 'Invitification') return { href: '/invites' }

View File

@ -4,6 +4,7 @@ import SearchIcon from '../svgs/search-line.svg'
import { useEffect, useRef, useState } from 'react'
import { Form, Input, Select, DatePicker, SubmitButton } from './form'
import { useRouter } from 'next/router'
import { dayMonthYear, whenToFrom } from '../lib/time'
export default function Search ({ sub }) {
const router = useRouter()
@ -36,6 +37,8 @@ export default function Search ({ sub }) {
if (values.sort === '' || values.sort === 'zaprank') delete values.sort
if (values.when === '' || values.when === 'forever') delete values.when
if (values.when !== 'custom') { delete values.from; delete values.to }
if (values.from && !values.to) return
await router.push({
pathname: prefix + '/search',
query: values
@ -47,21 +50,14 @@ export default function Search ({ sub }) {
const what = router.pathname.startsWith('/stackers') ? 'stackers' : router.query.what || 'all'
const sort = router.query.sort || 'zaprank'
const when = router.query.when || 'forever'
const from = router.query.from || new Date().toISOString()
const to = router.query.to || new Date().toISOString()
const [datePicker, setDatePicker] = useState(when === 'custom')
// The following state is needed for the date picker (and driven by the date picker).
// Substituting router.query or formik values would cause network lag and/or timezone issues.
const [range, setRange] = useState({ start: new Date(from), end: new Date(to) })
return (
<>
<div className={styles.searchSection}>
<Container className={`px-md-0 ${styles.searchContainer}`}>
<Form
initial={{ q, what, sort, when, from, to }}
onSubmit={search}
initial={{ q, what, sort, when, from: '', to: '' }}
onSubmit={values => search({ ...values })}
>
<div className={`${styles.active} my-3`}>
<Input
@ -81,7 +77,7 @@ export default function Search ({ sub }) {
<SearchIcon width={22} height={22} />
</SubmitButton>
</div>
{filter &&
{filter && router.query.q &&
<div className='text-muted fw-bold d-flex align-items-center flex-wrap pb-2'>
<div className='text-muted fw-bold d-flex align-items-center pb-2'>
<Select
@ -107,9 +103,8 @@ export default function Search ({ sub }) {
<Select
groupClassName='mb-0 mx-2'
onChange={(formik, e) => {
search({ ...formik?.values, when: e.target.value, from: from || new Date().toISOString(), to: to || new Date().toISOString() })
setDatePicker(e.target.value === 'custom')
if (e.target.value === 'custom') setRange({ start: new Date(), end: new Date() })
const range = e.target.value === 'custom' ? { from: whenToFrom(when), to: dayMonthYear(new Date()) } : {}
search({ ...formik?.values, when: e.target.value, ...range })
}}
name='when'
size='sm'
@ -118,24 +113,17 @@ export default function Search ({ sub }) {
/>
</>}
</div>
{datePicker &&
{when === 'custom' &&
<DatePicker
fromName='from' toName='to'
className='form-control p-0 px-2 mb-2 text-center'
onMount={() => {
setRange({ start: new Date(from), end: new Date(to) })
return [from, to]
fromName='from'
toName='to'
className='p-0 px-2 mb-2'
onChange={(formik, [from, to], e) => {
search({ ...formik?.values, from, to })
}}
onChange={(formik, [start, end], e) => {
setRange({ start, end })
search({ ...formik?.values, from: start && start.toISOString(), to: end && end.toISOString() })
}}
selected={range.start}
startDate={range.start} endDate={range.end}
selectsRange
dateFormat='MM/dd/yy'
maxDate={new Date()}
minDate={new Date('2021-05-01')}
from={router.query.from}
to={router.query.to}
when={when}
/>}
</div>}
</Form>

View File

@ -1,6 +1,7 @@
import { useRouter } from 'next/router'
import { Form, Select } from './form'
import { Form, Select, DatePicker } from './form'
import { ITEM_SORTS, USER_SORTS, WHENS } from '../lib/constants'
import { dayMonthYear, whenToFrom } from '../lib/time'
export default function TopHeader ({ sub, cat }) {
const router = useRouter()
@ -24,6 +25,8 @@ export default function TopHeader ({ sub, cat }) {
delete query.by
}
}
if (when !== 'custom') { delete query.from; delete query.to }
if (query.from && !query.to) return
await router.push({
pathname: `${prefix}/top/${what}/${when || 'day'}`,
@ -39,41 +42,58 @@ export default function TopHeader ({ sub, cat }) {
<div className='d-flex'>
<Form
className='me-auto'
initial={{ what, by, when }}
initial={{ what, by, when, from: '', to: '' }}
onSubmit={top}
>
<div className='text-muted fw-bold my-3 d-flex align-items-center'>
top
<Select
groupClassName='mx-2 mb-0'
onChange={(formik, e) => top({ ...formik?.values, what: e.target.value })}
name='what'
size='sm'
overrideValue={what}
items={router?.query?.sub ? ['posts', 'comments'] : ['posts', 'comments', 'stackers', 'cowboys']}
/>
{cat !== 'cowboys' &&
<>
by
<Select
groupClassName='mx-2 mb-0'
onChange={(formik, e) => top({ ...formik?.values, by: e.target.value })}
name='by'
size='sm'
overrideValue={by}
items={cat === 'stackers' ? USER_SORTS : ITEM_SORTS}
/>
for
<Select
groupClassName='mb-0 ms-2'
onChange={(formik, e) => top({ ...formik?.values, when: e.target.value })}
name='when'
size='sm'
overrideValue={when}
items={WHENS}
/>
</>}
<div className='text-muted fw-bold my-3 d-flex align-items-center flex-wrap'>
<div className='text-muted fw-bold my-2 d-flex align-items-center'>
top
<Select
groupClassName='mx-2 mb-0'
onChange={(formik, e) => top({ ...formik?.values, what: e.target.value })}
name='what'
size='sm'
overrideValue={what}
items={router?.query?.sub ? ['posts', 'comments'] : ['posts', 'comments', 'stackers', 'cowboys']}
/>
{cat !== 'cowboys' &&
<>
by
<Select
groupClassName='mx-2 mb-0'
onChange={(formik, e) => top({ ...formik?.values, by: e.target.value })}
name='by'
size='sm'
overrideValue={by}
items={cat === 'stackers' ? USER_SORTS : ITEM_SORTS}
/>
for
<Select
groupClassName='mb-0 mx-2'
onChange={(formik, e) => {
const range = e.target.value === 'custom' ? { from: whenToFrom(when), to: dayMonthYear(new Date()) } : {}
top({ ...formik?.values, when: e.target.value, ...range })
}}
name='when'
size='sm'
overrideValue={when}
items={WHENS}
/>
</>}
</div>
{when === 'custom' &&
<DatePicker
fromName='from'
toName='to'
className='p-0 px-2 my-2'
onChange={(formik, [from, to], e) => {
top({ ...formik?.values, from, to })
}}
from={router.query.from}
to={router.query.to}
when={when}
/>}
</div>
</Form>
</div>

View File

@ -1,23 +1,56 @@
import { useRouter } from 'next/router'
import { Select } from './form'
import { Select, DatePicker } from './form'
import { WHENS } from '../lib/constants'
import { dayMonthYear, whenToFrom } from '../lib/time'
export function UsageHeader () {
const router = useRouter()
const select = async values => {
const { when, ...query } = values
if (when !== 'custom') { delete query.from; delete query.to }
if (query.from && !query.to) return
await router.push({
pathname: `/stackers/${when}`,
query
})
}
const when = router.query.when || 'day'
return (
<div className='text-muted fw-bold my-3 d-flex align-items-center'>
stacker analytics for
<Select
groupClassName='mb-0 ms-2'
className='w-auto'
name='when'
size='sm'
items={WHENS}
value={router.query.when || 'day'}
noForm
onChange={(formik, e) => router.push(`/stackers/${e.target.value}`)}
/>
<div className='text-muted fw-bold my-0 d-flex align-items-center flex-wrap'>
<div className='text-muted fw-bold my-2 d-flex align-items-center'>
stacker analytics for
<Select
groupClassName='mb-0 mx-2'
className='w-auto'
name='when'
size='sm'
items={WHENS}
value={when}
noForm
onChange={(formik, e) => {
const range = e.target.value === 'custom' ? { from: whenToFrom(when), to: dayMonthYear(new Date()) } : {}
select({ when: e.target.value, ...range })
}}
/>
</div>
{when === 'custom' &&
<DatePicker
noForm
fromName='from'
toName='to'
className='p-0 px-2 mb-0'
onChange={(formik, [from, to], e) => {
select({ when, from, to })
}}
from={router.query.from}
to={router.query.to}
when={when}
/>}
</div>
)
}

View File

@ -24,12 +24,12 @@ export const SUB_ITEMS = gql`
${ITEM_FIELDS}
${COMMENTS_ITEM_EXT_FIELDS}
query SubItems($sub: String, $sort: String, $cursor: String, $type: String, $name: String, $when: String, $by: String, $limit: Int, $includeComments: Boolean = false) {
query SubItems($sub: String, $sort: String, $cursor: String, $type: String, $name: String, $when: String, $from: String, $to: String, $by: String, $limit: Int, $includeComments: Boolean = false) {
sub(name: $sub) {
...SubFields
}
items(sub: $sub, sort: $sort, cursor: $cursor, type: $type, name: $name, when: $when, by: $by, limit: $limit) {
items(sub: $sub, sort: $sort, cursor: $cursor, type: $type, name: $name, when: $when, from: $from, to: $to, by: $by, limit: $limit) {
cursor
items {
...ItemFields

View File

@ -166,19 +166,19 @@ export const USER_FIELDS = gql`
}`
export const TOP_USERS = gql`
query TopUsers($cursor: String, $when: String, $by: String, $limit: Int) {
topUsers(cursor: $cursor, when: $when, by: $by, limit: $limit) {
query TopUsers($cursor: String, $when: String, $from: String, $to: String, $by: String, $limit: Int) {
topUsers(cursor: $cursor, when: $when, from: $from, to: $to, by: $by, limit: $limit) {
users {
id
name
streak
hideCowboyHat
photoId
stacked(when: $when)
spent(when: $when)
ncomments(when: $when)
nposts(when: $when)
referrals(when: $when)
stacked(when: $when, from: $from, to: $to)
spent(when: $when, from: $from, to: $to)
ncomments(when: $when, from: $from, to: $to)
nposts(when: $when, from: $from, to: $to)
referrals(when: $when, from: $from, to: $to)
}
cursor
}
@ -233,11 +233,11 @@ export const USER_WITH_ITEMS = gql`
${USER_FIELDS}
${ITEM_FIELDS}
${COMMENTS_ITEM_EXT_FIELDS}
query UserWithItems($name: String!, $sub: String, $cursor: String, $type: String, $when: String, $by: String, $limit: Int, $includeComments: Boolean = false) {
query UserWithItems($name: String!, $sub: String, $cursor: String, $type: String, $when: String, $from: String, $to: String, $by: String, $limit: Int, $includeComments: Boolean = false) {
user(name: $name) {
...UserFields
}
items(sub: $sub, sort: "user", cursor: $cursor, type: $type, name: $name, when: $when, by: $by, limit: $limit) {
items(sub: $sub, sort: "user", cursor: $cursor, type: $type, name: $name, when: $when, from: $from, to: $to, by: $by, limit: $limit) {
cursor
items {
...ItemFields

View File

@ -35,7 +35,7 @@ function getClient (uri) {
Query: {
fields: {
topUsers: {
keyArgs: ['when', 'by'],
keyArgs: ['when', 'by', 'from', 'to', 'limit'],
merge (existing, incoming) {
if (isFirstPage(incoming.cursor, existing?.users)) {
return incoming
@ -61,7 +61,7 @@ function getClient (uri) {
}
},
items: {
keyArgs: ['sub', 'sort', 'type', 'name', 'when', 'by'],
keyArgs: ['sub', 'sort', 'type', 'name', 'when', 'by', 'from', 'to', 'limit'],
merge (existing, incoming) {
if (isFirstPage(incoming.cursor, existing?.items)) {
return incoming
@ -94,7 +94,7 @@ function getClient (uri) {
}
},
search: {
keyArgs: ['q', 'sub', 'sort', 'what', 'when'],
keyArgs: ['q', 'sub', 'sort', 'what', 'when', 'from', 'to', 'limit'],
merge (existing, incoming) {
if (isFirstPage(incoming.cursor, existing?.items)) {
return incoming

View File

@ -34,7 +34,7 @@ export const DONT_LIKE_THIS_COST = 1
export const COMMENT_TYPE_QUERY = ['comments', 'freebies', 'outlawed', 'borderland', 'all', 'bookmarks']
export const USER_SORTS = ['stacked', 'spent', 'comments', 'posts', 'referrals']
export const ITEM_SORTS = ['zaprank', 'comments', 'sats']
export const WHENS = ['day', 'week', 'month', 'year', 'forever']
export const WHENS = ['day', 'week', 'month', 'year', 'forever', 'custom']
export const ITEM_TYPES = context => {
const items = ['all', 'posts', 'comments', 'bounties', 'links', 'discussions', 'polls']
if (!context) {

View File

@ -34,6 +34,10 @@ export function datePivot (date,
}
export const dayMonthYear = when => new Date(when).toISOString().slice(0, 10)
export const dayMonthYearToDate = when => {
const [year, month, day] = when.split('-')
return new Date(+year, month - 1, day)
}
export function timeLeft (timeStamp) {
const now = new Date()
@ -57,4 +61,40 @@ export function timeLeft (timeStamp) {
}
}
export function timeUnitForRange ([from, to]) {
const date1 = new Date(from)
const date2 = new Date(to)
const diffTime = Math.abs(date2 - date1)
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
if (diffDays < 7) {
return 'hour'
}
if (diffDays < 90) {
return 'day'
}
if (diffDays < 180) {
return 'week'
}
return 'month'
}
export const whenToFrom = (when) => {
switch (when) {
case 'day':
return dayMonthYear(new Date(), { hours: -24 })
case 'week':
return dayMonthYear(datePivot(new Date(), { days: -7 }))
case 'month':
return dayMonthYear(datePivot(new Date(), { days: -30 }))
case 'year':
return dayMonthYear(datePivot(new Date(), { days: -365 }))
default:
return dayMonthYear(new Date('2021-05-01'))
}
}
export const sleep = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms))

View File

@ -6,7 +6,8 @@ import { useQuery } from '@apollo/client'
import { COMMENT_TYPE_QUERY, ITEM_SORTS, ITEM_TYPES, WHENS } from '../../lib/constants'
import PageLoading from '../../components/page-loading'
import { UserLayout } from '.'
import { Form, Select } from '../../components/form'
import { Form, Select, DatePicker } from '../../components/form'
import { dayMonthYear, whenToFrom } from '../../lib/time'
const staticVariables = { sort: 'user' }
const variablesFunc = vars => ({
@ -47,6 +48,8 @@ function UserItemsHeader ({ type, name }) {
if (!type || type === 'all' || !ITEM_TYPES('user').includes(type)) type = 'all'
if (!query.by || query.by === 'recent' || !ITEM_SORTS.includes(query.by)) delete query.by
if (!query.when || query.when === 'forever' || !WHENS.includes(query.when) || query.when === 'forever') delete query.when
if (query.when !== 'custom') { delete query.from; delete query.to }
if (query.from && !query.to) return
await router.push({
pathname: `/${name}/${type}`,
@ -60,39 +63,55 @@ function UserItemsHeader ({ type, name }) {
return (
<Form
initial={{ type, by, when }}
initial={{ type, by, when, from: '', to: '' }}
onSubmit={select}
>
<div className='text-muted fw-bold mt-0 mb-3 d-flex justify-content-start align-items-center'>
<Select
groupClassName='mb-0 me-2'
className='w-auto'
name='type'
size='sm'
overrideValue={type}
items={ITEM_TYPES('user')}
onChange={(formik, e) => select({ ...formik?.values, type: e.target.value })}
/>
by
<Select
groupClassName='mb-0 mx-2'
className='w-auto'
name='by'
size='sm'
overrideValue={by}
items={['recent', ...ITEM_SORTS]}
onChange={(formik, e) => select({ ...formik?.values, by: e.target.value })}
/>
for
<Select
groupClassName='mb-0 ms-2'
className='w-auto'
name='when'
size='sm'
items={WHENS}
overrideValue={when}
onChange={(formik, e) => select({ ...formik?.values, when: e.target.value })}
/>
<div className='text-muted fw-bold mt-0 mb-3 d-flex justify-content-start align-items-center flex-wrap'>
<div className='text-muted fw-bold mt-0 mb-2 d-flex justify-content-start align-items-center'>
<Select
groupClassName='mb-0 me-2'
className='w-auto'
name='type'
size='sm'
overrideValue={type}
items={ITEM_TYPES('user')}
onChange={(formik, e) => select({ ...formik?.values, type: e.target.value })}
/>
by
<Select
groupClassName='mb-0 mx-2'
className='w-auto'
name='by'
size='sm'
overrideValue={by}
items={['recent', ...ITEM_SORTS]}
onChange={(formik, e) => select({ ...formik?.values, by: e.target.value })}
/>
for
<Select
groupClassName='mb-0 mx-2'
className='w-auto'
name='when'
size='sm'
items={WHENS}
overrideValue={when}
onChange={(formik, e) => {
const range = e.target.value === 'custom' ? { from: whenToFrom(when), to: dayMonthYear(new Date()) } : {}
select({ ...formik?.values, when: e.target.value, ...range })
}}
/>
</div>
{when === 'custom' &&
<DatePicker
fromName='from' toName='to'
className='p-0 px-2 mb-2'
onChange={(formik, [from, to], e) => {
select({ ...formik?.values, from, to })
}}
from={router.query.from}
to={router.query.to}
when={when}
/>}
</div>
</Form>
)

View File

@ -2,7 +2,7 @@ import { gql } from 'graphql-tag'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { getGetServerSideProps } from '../../api/ssrApollo'
import { CopyInput, Select } from '../../components/form'
import { CopyInput, Select, DatePicker } from '../../components/form'
import { CenterLayout } from '../../components/layout'
import { useMe } from '../../components/me'
import { useQuery } from '@apollo/client'
@ -10,15 +10,16 @@ import PageLoading from '../../components/page-loading'
import { WHENS } from '../../lib/constants'
import dynamic from 'next/dynamic'
import { numWithUnits } from '../../lib/format'
import { dayMonthYear, whenToFrom } from '../../lib/time'
const WhenComposedChart = dynamic(() => import('../../components/charts').then(mod => mod.WhenComposedChart), {
loading: () => <div>Loading...</div>
})
const REFERRALS = gql`
query Referrals($when: String!)
query Referrals($when: String!, $from: String, $to: String)
{
referrals(when: $when) {
referrals(when: $when, from: $from, to: $to) {
totalSats
totalReferrals
stats {
@ -37,26 +38,58 @@ export default function Referrals ({ ssrData }) {
const router = useRouter()
const me = useMe()
const { data } = useQuery(REFERRALS, { variables: { when: router.query.when } })
const select = async values => {
const { when, ...query } = values
if (when !== 'custom') { delete query.from; delete query.to }
if (query.from && !query.to) return
await router.push({
pathname: `/referrals/${when}`,
query
})
}
const { data } = useQuery(REFERRALS, { variables: { when: router.query.when, from: router.query.from, to: router.query.to } })
if (!data && !ssrData) return <PageLoading />
const { referrals: { totalSats, totalReferrals, stats } } = data || ssrData
const when = router.query.when
return (
<CenterLayout footerLinks>
<h4 className='fw-bold text-muted text-center pt-5 pb-3 d-flex align-items-center justify-content-center'>
{numWithUnits(totalReferrals, { unitPlural: 'referrals', unitSingular: 'referral' })} & {numWithUnits(totalSats, { abbreviate: false })} in the last
<Select
groupClassName='mb-0 ms-2'
className='w-auto'
name='when'
size='sm'
items={WHENS}
value={router.query.when || 'day'}
noForm
onChange={(formik, e) => router.push(`/referrals/${e.target.value}`)}
/>
</h4>
<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'>
{numWithUnits(totalReferrals, { unitPlural: 'referrals', unitSingular: 'referral' })} & {numWithUnits(totalSats, { abbreviate: false })} in the last
<Select
groupClassName='mb-0 mx-2'
className='w-auto'
name='when'
size='sm'
items={WHENS}
value={router.query.when || 'day'}
noForm
onChange={(formik, e) => {
const range = e.target.value === 'custom' ? { from: whenToFrom(when), to: dayMonthYear(new Date()) } : {}
select({ when: e.target.value, ...range })
}}
/>
</h4>
{when === 'custom' &&
<DatePicker
noForm
fromName='from'
toName='to'
className='p-0 px-2 mb-2'
onChange={(formik, [from, to], e) => {
select({ when, from, to })
}}
from={router.query.from}
to={router.query.to}
when={router.query.when}
/>}
</div>
<WhenComposedChart data={stats} lineNames={['sats']} barNames={['referrals']} barAxis='right' />
<div

View File

@ -19,44 +19,44 @@ const WhenComposedChart = dynamic(() => import('../../components/charts').then(m
})
const GROWTH_QUERY = gql`
query Growth($when: String!)
query Growth($when: String!, $from: String, $to: String)
{
registrationGrowth(when: $when) {
registrationGrowth(when: $when, from: $from, to: $to) {
time
data {
name
value
}
}
itemGrowth(when: $when) {
itemGrowth(when: $when, from: $from, to: $to) {
time
data {
name
value
}
}
spendingGrowth(when: $when) {
spendingGrowth(when: $when, from: $from, to: $to) {
time
data {
name
value
}
}
spenderGrowth(when: $when) {
spenderGrowth(when: $when, from: $from, to: $to) {
time
data {
name
value
}
}
stackingGrowth(when: $when) {
stackingGrowth(when: $when, from: $from, to: $to) {
time
data {
name
value
}
}
stackerGrowth(when: $when) {
stackerGrowth(when: $when, from: $from, to: $to) {
time
data {
name
@ -69,10 +69,10 @@ export const getServerSideProps = getGetServerSideProps({ query: GROWTH_QUERY })
export default function Growth ({ ssrData }) {
const router = useRouter()
const { when } = router.query
const { when, from, to } = router.query
const avg = ['year', 'forever'].includes(when) ? 'avg daily ' : ''
const { data } = useQuery(GROWTH_QUERY, { variables: { when } })
const { data } = useQuery(GROWTH_QUERY, { variables: { when, from, to } })
if (!data && !ssrData) return <PageLoading />
const { registrationGrowth, itemGrowth, spendingGrowth, spenderGrowth, stackingGrowth, stackerGrowth } = data || ssrData

View File

@ -7,8 +7,9 @@ import { SUB_ITEMS } from '../../../../fragments/subs'
import { COMMENT_TYPE_QUERY } from '../../../../lib/constants'
const staticVariables = { sort: 'top' }
const variablesFunc = vars =>
({ includeComments: COMMENT_TYPE_QUERY.includes(vars.type), ...staticVariables, ...vars })
const variablesFunc = vars => {
return ({ includeComments: COMMENT_TYPE_QUERY.includes(vars.type), ...staticVariables, ...vars })
}
export const getServerSideProps = getGetServerSideProps({
query: SUB_ITEMS,
variables: variablesFunc,

View File

@ -855,4 +855,12 @@ div[contenteditable]:focus,
.popover-body {
color: var(--bs-body-color);
}
}
}
// To satisfy assumptions of the date picker component.
.react-datepicker__navigation-icon {
line-height: normal;
}
.react-datepicker__navigation-icon::before, .react-datepicker__navigation-icon::after {
box-sizing: content-box;
}

View File

@ -106,7 +106,7 @@ export function indexAllItems ({ apollo }) {
query: gql`
${ITEM_SEARCH_FIELDS}
query AllItems($cursor: String) {
items(cursor: $cursor, sort: "recent", limit: 100, type: "all") {
items(cursor: $cursor, sort: "recent", limit: 1000, type: "all") {
items {
...ItemSearchFields
}