Non-quad tip UI (WIP)
This commit is contained in:
parent
67d0a6c0c2
commit
4f28743252
|
@ -1,3 +1,4 @@
|
||||||
|
import { XIcon } from '@heroicons/react/outline'
|
||||||
import {
|
import {
|
||||||
ChevronDoubleRightIcon,
|
ChevronDoubleRightIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
|
@ -7,7 +8,7 @@ import clsx from 'clsx'
|
||||||
import { Comment } from 'common/comment'
|
import { Comment } from 'common/comment'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { debounce, sumBy } from 'lodash'
|
import { debounce, sum, sumBy } from 'lodash'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { CommentTips } from 'web/hooks/use-tip-txns'
|
import { CommentTips } from 'web/hooks/use-tip-txns'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
@ -16,33 +17,24 @@ import { track } from 'web/lib/service/analytics'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { Tooltip } from './tooltip'
|
import { Tooltip } from './tooltip'
|
||||||
|
|
||||||
// xth triangle number * 5 = 5 + 10 + 15 + ... + (x * 5)
|
|
||||||
const quad = (x: number) => (5 / 2) * x * (x + 1)
|
|
||||||
|
|
||||||
// inverse (see https://math.stackexchange.com/questions/2041988/how-to-get-inverse-of-formula-for-sum-of-integers-from-1-to-nsee )
|
|
||||||
const invQuad = (y: number) => Math.sqrt((2 / 5) * y + 1 / 4) - 1 / 2
|
|
||||||
|
|
||||||
export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
||||||
const { comment, tips } = prop
|
const { comment, tips } = prop
|
||||||
|
|
||||||
const me = useUser()
|
const me = useUser()
|
||||||
const myId = me?.id ?? ''
|
const myId = me?.id ?? ''
|
||||||
const savedTip = tips[myId] as number | undefined
|
const savedTip = tips[myId] ?? 0
|
||||||
|
|
||||||
// optimistically increase the tip count, but debounce the update
|
const [localTip, setLocalTip] = useState(savedTip)
|
||||||
const [localTip, setLocalTip] = useState(savedTip ?? 0)
|
// listen for user being set
|
||||||
const initialized = useRef(false)
|
const initialized = useRef(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (savedTip && !initialized.current) {
|
if (tips[myId] && !initialized.current) {
|
||||||
setLocalTip(savedTip)
|
setLocalTip(tips[myId])
|
||||||
initialized.current = true
|
initialized.current = true
|
||||||
}
|
}
|
||||||
}, [savedTip])
|
}, [tips, myId])
|
||||||
|
|
||||||
const score = useMemo(() => {
|
const total = sum(Object.values(tips)) - savedTip + localTip
|
||||||
const tipVals = Object.values({ ...tips, [myId]: localTip })
|
|
||||||
return sumBy(tipVals, invQuad)
|
|
||||||
}, [localTip, tips, myId])
|
|
||||||
|
|
||||||
// declare debounced function only on first render
|
// declare debounced function only on first render
|
||||||
const [saveTip] = useState(() =>
|
const [saveTip] = useState(() =>
|
||||||
|
@ -80,87 +72,33 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
||||||
|
|
||||||
const changeTip = (tip: number) => {
|
const changeTip = (tip: number) => {
|
||||||
setLocalTip(tip)
|
setLocalTip(tip)
|
||||||
me && saveTip(me, tip - (savedTip ?? 0))
|
me && saveTip(me, tip - savedTip)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="items-center gap-0.5">
|
<Row className="items-center gap-2">
|
||||||
<DownTip
|
{total > 0 && <span className="font-normal">{total}</span>}
|
||||||
value={localTip}
|
|
||||||
onChange={changeTip}
|
<button
|
||||||
disabled={!me || localTip <= 0}
|
className="font-bold disabled:text-gray-300"
|
||||||
/>
|
disabled={
|
||||||
<span className="font-bold">{Math.floor(score)} </span>
|
!me ||
|
||||||
<UpTip
|
me.id === comment.userId ||
|
||||||
value={localTip}
|
me.balance < localTip - savedTip + 5
|
||||||
onChange={changeTip}
|
}
|
||||||
disabled={!me || me.id === comment.userId}
|
onClick={() => changeTip(localTip + 5)}
|
||||||
/>
|
|
||||||
{localTip === 0 ? (
|
|
||||||
''
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
'font-semibold',
|
|
||||||
localTip > 0 ? 'text-primary' : 'text-red-400'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
({formatMoney(localTip)} tip)
|
Tip
|
||||||
</span>
|
</button>
|
||||||
|
{localTip > 0 && (
|
||||||
|
<span className="text-primary font-semibold">(+{localTip})</span>
|
||||||
|
)}
|
||||||
|
{/* undo button */}
|
||||||
|
{localTip > 0 && (
|
||||||
|
<button className="text-red-500" onClick={() => changeTip(0)}>
|
||||||
|
<XIcon className="w-4" />
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DownTip(prop: {
|
|
||||||
value: number
|
|
||||||
onChange: (tip: number) => void
|
|
||||||
disabled?: boolean
|
|
||||||
}) {
|
|
||||||
const { onChange, value, disabled } = prop
|
|
||||||
const marginal = 5 * invQuad(value)
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
className="tooltip-bottom"
|
|
||||||
text={!disabled && `Refund ${formatMoney(marginal)}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="flex h-max items-center hover:text-red-600 disabled:text-gray-300"
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={() => onChange(value - marginal)}
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpTip(prop: {
|
|
||||||
value: number
|
|
||||||
onChange: (tip: number) => void
|
|
||||||
disabled?: boolean
|
|
||||||
}) {
|
|
||||||
const { onChange, value, disabled } = prop
|
|
||||||
const marginal = 5 * invQuad(value) + 5
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
className="tooltip-bottom"
|
|
||||||
text={!disabled && `Tip ${formatMoney(marginal)}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="hover:text-primary flex h-max items-center disabled:text-gray-300"
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={() => onChange(value + marginal)}
|
|
||||||
>
|
|
||||||
{value >= quad(2) ? (
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user