diff --git a/backend/api/src/get-balance-changes.ts b/backend/api/src/get-balance-changes.ts index e6dd2f9d87..542f3a8017 100644 --- a/backend/api/src/get-balance-changes.ts +++ b/backend/api/src/get-balance-changes.ts @@ -13,29 +13,36 @@ import { convertContract } from 'common/supabase/contracts' export const getBalanceChanges: APIHandler<'get-balance-changes'> = async ( props ) => { - const { after, userId } = props + const { userId, before, after } = props const [betBalanceChanges, txnBalanceChanges] = await Promise.all([ - getBetBalanceChanges(after, userId), - getTxnBalanceChanges(after, userId), + getBetBalanceChanges(before, after, userId), + getTxnBalanceChanges(before, after, userId), ]) - return orderBy( + const allChanges = orderBy( [...betBalanceChanges, ...txnBalanceChanges], (change) => change.createdTime, 'desc' ) + return allChanges } -const getTxnBalanceChanges = async (after: number, userId: string) => { +const getTxnBalanceChanges = async ( + before: number | undefined, + after: number, + userId: string +) => { const pg = createSupabaseDirectClient() const balanceChanges = [] as TxnBalanceChange[] const txns = await pg.map( `select * from txns - where created_time > millis_to_ts($1) - and (to_id = $2 or from_id = $2) + where + ($1 is null or created_time < millis_to_ts($1)) and + created_time >= millis_to_ts($2) + and (to_id = $3 or from_id = $3) order by created_time`, - [after, userId], + [before, after, userId], convertTxn ) const contractIds = filterDefined( @@ -113,7 +120,11 @@ const getContractIdFromTxn = (txn: Txn) => { return null } -const getBetBalanceChanges = async (after: number, userId: string) => { +const getBetBalanceChanges = async ( + before: number | undefined, + after: number, + userId: string +) => { const pg = createSupabaseDirectClient() const contractToBets: { [contractId: string]: { @@ -133,11 +144,13 @@ const getBetBalanceChanges = async (after: number, userId: string) => { from contract_bets cb join contracts c on cb.contract_id = c.id left join answers a on a.id = cb.answer_id - where cb.updated_time > millis_to_ts($1) - and cb.user_id = $2 + where + ($1 is null or cb.updated_time < millis_to_ts($1)) + and cb.updated_time >= millis_to_ts($2) + and cb.user_id = $3 group by c.id; `, - [after, userId], + [before, after, userId], (row) => { contractToBets[row.id] = { bets: orderBy(row.bets, (bet) => bet.createdTime, 'asc'), diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 60de34b26b..b79a3999ec 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -1342,7 +1342,8 @@ export const API = (_apiTypeCheck = { returns: [] as AnyBalanceChangeType[], props: z .object({ - after: z.coerce.number(), + before: z.coerce.number().optional(), + after: z.coerce.number().default(0), userId: z.string(), }) .strict(), diff --git a/web/components/portfolio/balance-change-table.tsx b/web/components/portfolio/balance-change-table.tsx index 6a15faec76..7e675557ca 100644 --- a/web/components/portfolio/balance-change-table.tsx +++ b/web/components/portfolio/balance-change-table.tsx @@ -1,3 +1,4 @@ +import dayjs from 'dayjs' import { Col } from 'web/components/layout/col' import { formatMoney, @@ -31,80 +32,161 @@ import { linkClass } from 'web/components/widgets/site-link' import { ScaleIcon } from '@heroicons/react/outline' import { QuestType } from 'common/quest' import { Input } from 'web/components/widgets/input' -import { formatJustTime, formatTimeShort } from 'web/lib/util/time' +import { + formatJustDateShort, + formatJustTime, + formatTimeShort, +} from 'web/lib/util/time' import { assertUnreachable } from 'common/util/types' import { AnyTxnCategory } from 'common/txn' import { useAPIGetter } from 'web/hooks/use-api-getter' import { Button } from 'web/components/buttons/button' import { Modal } from '../layout/modal' -export const BalanceChangeTable = (props: { - user: User - balanceChanges: AnyBalanceChangeType[] - simple?: boolean -}) => { - const { user, simple } = props +export const BalanceChangeTable = (props: { user: User }) => { + const { user } = props + + const [before, setBefore] = useState(undefined) + const [after, setAfter] = useState( + dayjs().startOf('day').subtract(14, 'day').valueOf() + ) + + const { data: allBalanceChanges } = useAPIGetter('get-balance-changes', { + userId: user.id, + before, + after, + }) + const [query, setQuery] = useState('') const { data: cashouts } = useAPIGetter('get-cashouts', { userId: user.id, }) const [showCashoutModal, setShowCashoutModal] = useState(false) - const balanceChanges = props.balanceChanges - .filter((change) => { - const { type } = change - const contractQuestion = - ('contract' in change && change.contract?.question) || '' - const changeType = type - const userName = 'user' in change ? change.user?.name ?? '' : '' - const userUsername = 'user' in change ? change.user?.username ?? '' : '' - const answerText = 'answer' in change ? change.answer?.text ?? '' : '' - const betText = 'bet' in change ? betChangeToText(change) : '' - return ( - contractQuestion.toLowerCase().includes(query.toLowerCase()) || - changeType.toLowerCase().includes(query.toLowerCase()) || - (txnTypeToDescription(changeType) || '') - .toLowerCase() - .includes(query.toLowerCase()) || - answerText.toLowerCase().includes(query.toLowerCase()) || - ((isTxnChange(change) && txnTitle(change)) || '') - .toLowerCase() - .includes(query.toLowerCase()) || - userName.toLowerCase().includes(query.toLowerCase()) || - userUsername.toLowerCase().includes(query.toLowerCase()) || - betText.toLowerCase().includes(query.toLowerCase()) - ) - }) - .slice(0, 1000) + const balanceChanges = (allBalanceChanges ?? []).filter((change) => { + const { type } = change + const contractQuestion = + ('contract' in change && change.contract?.question) || '' + const changeType = type + const userName = 'user' in change ? change.user?.name ?? '' : '' + const userUsername = 'user' in change ? change.user?.username ?? '' : '' + const answerText = 'answer' in change ? change.answer?.text ?? '' : '' + const betText = 'bet' in change ? betChangeToText(change) : '' + return ( + contractQuestion.toLowerCase().includes(query.toLowerCase()) || + changeType.toLowerCase().includes(query.toLowerCase()) || + (txnTypeToDescription(changeType) || '') + .toLowerCase() + .includes(query.toLowerCase()) || + answerText.toLowerCase().includes(query.toLowerCase()) || + ((isTxnChange(change) && txnTitle(change)) || '') + .toLowerCase() + .includes(query.toLowerCase()) || + userName.toLowerCase().includes(query.toLowerCase()) || + userUsername.toLowerCase().includes(query.toLowerCase()) || + betText.toLowerCase().includes(query.toLowerCase()) + ) + }) const pendingCashouts = cashouts?.filter((c) => c.txn.gidxStatus === 'Pending')?.length ?? 0 return ( - + setQuery(e.target.value)} /> - + + + + setAfter(dayjs(e.target.value).startOf('day').valueOf()) + } + /> + to + + setBefore( + e.target.value + ? dayjs(e.target.value).endOf('day').valueOf() + : undefined + ) + } + /> + {cashouts && cashouts.length > 0 && ( - - - + )} - - + + {!!before && before < Date.now() && ( + +
+ Cutoff: {formatJustDateShort(before)} + + + + +
+ + )} + + + +
+ Cutoff: {formatJustDateShort(after)} + + + + + +
+ + @@ -139,10 +221,9 @@ function RenderBalanceChanges(props: { balanceChanges: AnyBalanceChangeType[] user: User avatarSize: 'sm' | 'md' - simple?: boolean hideBalance?: boolean }) { - const { balanceChanges, user, avatarSize, simple, hideBalance } = props + const { balanceChanges, user, avatarSize, hideBalance } = props let currManaBalance = user.balance let currCashBalance = user.cashBalance let currSpiceBalance = user.spiceBalance @@ -178,7 +259,6 @@ function RenderBalanceChanges(props: { change={change} balance={balanceRunningTotals[i]} avatarSize={avatarSize} - simple={simple} hideBalance={hideBalance} token={change.contract.token} /> @@ -190,7 +270,6 @@ function RenderBalanceChanges(props: { change={change as TxnBalanceChange} balance={balanceRunningTotals[i]} avatarSize={avatarSize} - simple={simple} hideBalance={hideBalance} /> ) @@ -261,11 +340,10 @@ const BetBalanceChangeRow = (props: { change: BetBalanceChange balance: { mana: number; cash: number } avatarSize: 'sm' | 'md' - simple?: boolean hideBalance?: boolean token: 'MANA' | 'CASH' }) => { - const { change, balance, avatarSize, simple, hideBalance, token } = props + const { change, balance, avatarSize, hideBalance, token } = props const { amount, contract, answer, bet, type } = change const { outcome } = bet const { slug, question, creatorUsername } = contract @@ -345,19 +423,17 @@ const BetBalanceChangeRow = (props: { {betChangeToText(change)} {answer ? ` on ${answer.text}` : ''} - {!simple && ( - - {!hideBalance && ( - <> - {token === 'CASH' - ? formatSweepies(balance.cash) - : formatMoney(balance.mana)} - {'·'} - - )}{' '} - {customFormatTime(change.createdTime)} - - )} + + {!hideBalance && ( + <> + {token === 'CASH' + ? formatSweepies(balance.cash) + : formatMoney(balance.mana)} + {'·'} + + )}{' '} + {customFormatTime(change.createdTime)} + ) @@ -374,10 +450,9 @@ const TxnBalanceChangeRow = (props: { change: TxnBalanceChange balance: { mana: number; cash: number; spice: number } avatarSize: 'sm' | 'md' - simple?: boolean hideBalance?: boolean }) => { - const { change, balance, avatarSize, simple, hideBalance } = props + const { change, balance, avatarSize, hideBalance } = props const { contract, amount, type, token, user, charity, description } = change const reasonToBgClassNameMap: Partial<{ @@ -517,21 +592,19 @@ const TxnBalanceChangeRow = (props: {
{txnTypeToDescription(type) ?? description ?? type}
- {!simple && ( - - {!hideBalance && ( - <> - {token === 'SPICE' - ? formatSpice(balance.spice) - : token === 'CASH' - ? formatSweepies(balance.cash) - : formatMoney(balance.mana)} - {' · '} - - )} - {customFormatTime(change.createdTime)} - - )} + + {!hideBalance && ( + <> + {token === 'SPICE' + ? formatSpice(balance.spice) + : token === 'CASH' + ? formatSweepies(balance.cash) + : formatMoney(balance.mana)} + {' · '} + + )} + {customFormatTime(change.createdTime)} + ) diff --git a/web/lib/util/time.ts b/web/lib/util/time.ts index 5193b51c93..971e8b69b0 100644 --- a/web/lib/util/time.ts +++ b/web/lib/util/time.ts @@ -14,6 +14,10 @@ const FORMATTER = new Intl.DateTimeFormat('default', { export const formatTime = FORMATTER.format +export function formatJustDateShort(time: number) { + return dayjs(time).format('MMM D, YYYY') +} + export function formatTimeShort(time: number) { return dayjs(time).format('MMM D, h:mma') } diff --git a/web/pages/[username]/index.tsx b/web/pages/[username]/index.tsx index 0948498728..b30fb2b895 100644 --- a/web/pages/[username]/index.tsx +++ b/web/pages/[username]/index.tsx @@ -14,7 +14,6 @@ import { isUserLikelySpammer } from 'common/user' import { unauthedApi } from 'common/util/api' import { buildArray } from 'common/util/array' import { removeUndefinedProps } from 'common/util/object' -import dayjs from 'dayjs' import Head from 'next/head' import Link from 'next/link' import { useRouter } from 'next/router' @@ -213,14 +212,8 @@ function UserProfile(props: { } }, [currentUser?.id, user?.id]) - const { data: newBalanceChanges } = useAPIGetter('get-balance-changes', { - userId: user.id, - after: dayjs().startOf('day').subtract(14, 'day').valueOf(), - }) - - const balanceChanges = newBalanceChanges ?? [] - const hasBetBalanceChanges = balanceChanges.some((b) => isBetChange(b)) const balanceChangesKey = 'balance-changes' + return ( ), }, - (!!user.lastBetTime || hasBetBalanceChanges) && { + !!user.lastBetTime && { title: 'Trades', prerender: true, stackedTabIcon: , @@ -454,12 +447,7 @@ function UserProfile(props: { { title: 'Balance log', stackedTabIcon: , - content: ( - - ), + content: , queryString: balanceChangesKey, }, {