manifold/web/components/tipper.tsx
Marshall Polaris 4700ceb14c
Refactor some backend-related stuff (#639)
* web/lib/firebase/api-call -> common/api, web/lib/firebase/api

* Reuse `APIError` type in server code

* Reuse `getFunctionUrl` in server code
2022-07-10 15:03:15 -07:00

158 lines
4.1 KiB
TypeScript

import {
ChevronDoubleRightIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from '@heroicons/react/solid'
import clsx from 'clsx'
import { Comment } from 'common/comment'
import { User } from 'common/user'
import { formatMoney } from 'common/util/format'
import { debounce, sum } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { CommentTips } from 'web/hooks/use-tip-txns'
import { useUser } from 'web/hooks/use-user'
import { transact } from 'web/lib/firebase/api'
import { track } from 'web/lib/service/analytics'
import { Row } from './layout/row'
import { Tooltip } from './tooltip'
export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
const { comment, tips } = prop
const me = useUser()
const myId = me?.id ?? ''
const savedTip = tips[myId] ?? 0
const [localTip, setLocalTip] = useState(savedTip)
// listen for user being set
const initialized = useRef(false)
useEffect(() => {
if (tips[myId] && !initialized.current) {
setLocalTip(tips[myId])
initialized.current = true
}
}, [tips, myId])
const total = sum(Object.values(tips)) - savedTip + localTip
// declare debounced function only on first render
const [saveTip] = useState(() =>
debounce(async (user: User, change: number) => {
if (change === 0) {
return
}
await transact({
amount: change,
fromId: user.id,
fromType: 'USER',
toId: comment.userId,
toType: 'USER',
token: 'M$',
category: 'TIP',
data: {
contractId: comment.contractId,
commentId: comment.id,
groupId: comment.groupId,
},
description: `${user.name} tipped M$ ${change} to ${comment.userName} for a comment`,
})
track('send comment tip', {
contractId: comment.contractId,
commentId: comment.id,
groupId: comment.groupId,
amount: change,
fromId: user.id,
toId: comment.userId,
})
}, 1500)
)
// instant save on unrender
useEffect(() => () => void saveTip.flush(), [saveTip])
const changeTip = (tip: number) => {
setLocalTip(tip)
me && saveTip(me, tip - savedTip)
}
return (
<Row className="items-center gap-0.5">
<DownTip
value={localTip}
onChange={changeTip}
disabled={!me || localTip <= savedTip}
/>
<span className="font-bold">{Math.floor(total)}</span>
<UpTip
value={localTip}
onChange={changeTip}
disabled={!me || me.id === comment.userId || me.balance < localTip + 5}
/>
{localTip === 0 ? (
''
) : (
<span
className={clsx(
'font-semibold',
localTip > 0 ? 'text-primary' : 'text-red-400'
)}
>
({formatMoney(localTip)} tip)
</span>
)}
</Row>
)
}
function DownTip(prop: {
value: number
onChange: (tip: number) => void
disabled?: boolean
}) {
const { onChange, value, disabled } = prop
return (
<Tooltip
className="tooltip-bottom"
text={!disabled && `-${formatMoney(5)}`}
>
<button
className="flex h-max items-center hover:text-red-600 disabled:text-gray-300"
disabled={disabled}
onClick={() => onChange(value - 5)}
>
<ChevronLeftIcon className="h-6 w-6" />
</button>
</Tooltip>
)
}
function UpTip(prop: {
value: number
onChange: (tip: number) => void
disabled?: boolean
}) {
const { onChange, value, disabled } = prop
return (
<Tooltip
className="tooltip-bottom"
text={!disabled && `Tip ${formatMoney(5)}`}
>
<button
className="hover:text-primary flex h-max items-center disabled:text-gray-300"
disabled={disabled}
onClick={() => onChange(value + 5)}
>
{value >= 10 ? (
<ChevronDoubleRightIcon className="text-primary mx-1 h-6 w-6" />
) : value > 0 ? (
<ChevronRightIcon className="text-primary h-6 w-6" />
) : (
<ChevronRightIcon className="h-6 w-6" />
)}
</button>
</Tooltip>
)
}