added cancelling tipping and lil coin (#1047)

* added cancelling tipping and lil coin
* forced timeout to fix weird toast bug
This commit is contained in:
ingawei 2022-10-14 01:47:51 -05:00 committed by GitHub
parent 3f8988bf27
commit 4359ad0530
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 169 additions and 39 deletions

View File

@ -6,3 +6,4 @@ export type Like = {
tipTxnId?: string // only holds most recent tip txn id tipTxnId?: string // only holds most recent tip txn id
} }
export const LIKE_TIP_AMOUNT = 10 export const LIKE_TIP_AMOUNT = 10
export const TIP_UNDO_DURATION = 2000

View File

@ -108,7 +108,7 @@ export function IconButton(props: {
className={clsx( className={clsx(
'inline-flex items-center justify-center transition-colors disabled:cursor-not-allowed', 'inline-flex items-center justify-center transition-colors disabled:cursor-not-allowed',
sizeClasses[size], sizeClasses[size],
'disabled:text-greyscale-2 text-greyscale-6 hover:text-indigo-600', 'disabled:text-greyscale-2 text-greyscale-5 hover:text-greyscale-6',
className className
)} )}
disabled={disabled || loading} disabled={disabled || loading}

View File

@ -3,13 +3,13 @@ import { Contract } from 'common/contract'
import { User } from 'common/user' import { User } from 'common/user'
import { useUserLikes } from 'web/hooks/use-likes' import { useUserLikes } from 'web/hooks/use-likes'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { formatMoney } from 'common/util/format'
import { likeContract } from 'web/lib/firebase/likes' import { likeContract } from 'web/lib/firebase/likes'
import { LIKE_TIP_AMOUNT } from 'common/like' import { LIKE_TIP_AMOUNT, TIP_UNDO_DURATION } from 'common/like'
import { firebaseLogin } from 'web/lib/firebase/users' import { firebaseLogin } from 'web/lib/firebase/users'
import { useMarketTipTxns } from 'web/hooks/use-tip-txns' import { useMarketTipTxns } from 'web/hooks/use-tip-txns'
import { sum } from 'lodash' import { sum } from 'lodash'
import { TipButton } from './tip-button' import { TipButton } from './tip-button'
import { TipToast } from '../tipper'
export function LikeMarketButton(props: { export function LikeMarketButton(props: {
contract: Contract contract: Contract
@ -35,8 +35,20 @@ export function LikeMarketButton(props: {
if (!user) return firebaseLogin() if (!user) return firebaseLogin()
setIsLiking(true) setIsLiking(true)
const timeoutId = setTimeout(() => {
likeContract(user, contract).catch(() => setIsLiking(false)) likeContract(user, contract).catch(() => setIsLiking(false))
toast(`You tipped ${contract.creatorName} ${formatMoney(LIKE_TIP_AMOUNT)}!`) }, 3000)
toast.custom(
() => (
<TipToast
userName={contract.creatorUsername}
onUndoClick={() => {
clearTimeout(timeoutId)
}}
/>
),
{ duration: TIP_UNDO_DURATION }
)
} }
return ( return (

View File

@ -4,6 +4,7 @@ import { Col } from 'web/components/layout/col'
import { Tooltip } from '../tooltip' import { Tooltip } from '../tooltip'
import TipJar from 'web/public/custom-components/tipJar' import TipJar from 'web/public/custom-components/tipJar'
import { useState } from 'react' import { useState } from 'react'
import Coin from 'web/public/custom-components/coin'
export function TipButton(props: { export function TipButton(props: {
tipAmount: number tipAmount: number
@ -23,7 +24,7 @@ export function TipButton(props: {
<Tooltip <Tooltip
text={ text={
disabled disabled
? `Tips (${formatMoney(totalTipped)})` ? `Total tips ${formatMoney(totalTipped)}`
: `Tip ${formatMoney(tipAmount)}` : `Tip ${formatMoney(tipAmount)}`
} }
placement="bottom" placement="bottom"
@ -35,16 +36,44 @@ export function TipButton(props: {
disabled={disabled} disabled={disabled}
className={clsx( className={clsx(
'px-2 py-1 text-xs', //2xs button 'px-2 py-1 text-xs', //2xs button
'text-greyscale-6 transition-transform hover:text-indigo-600 disabled:cursor-not-allowed', 'text-greyscale-5 transition-transform disabled:cursor-not-allowed',
!disabled ? 'hover:rotate-12' : '' !disabled ? 'hover:text-greyscale-6' : ''
)} )}
onMouseOver={() => setHover(true)} onMouseOver={() => {
if (!disabled) {
setHover(true)
}
}}
onMouseLeave={() => setHover(false)} onMouseLeave={() => setHover(false)}
> >
<Col className={clsx('relative', disabled ? 'opacity-30' : '')}> <Col className={clsx('relative')}>
<div
className={clsx(
'absolute transition-all',
hover ? 'left-[6px] -top-[9px]' : 'left-[8px] -top-[10px]'
)}
>
<Coin
size={10}
color={
hover && !userTipped
? '#66667C'
: userTipped
? '#4f46e5'
: '#9191a7'
}
strokeWidth={2}
/>
</div>
<TipJar <TipJar
size={18} size={18}
color={userTipped || (hover && !disabled) ? '#4f46e5' : '#66667C'} color={
hover && !disabled && !userTipped
? '#66667C'
: userTipped
? '#4f46e5'
: '#9191a7'
}
/> />
<div <div
className={clsx( className={clsx(

View File

@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from 'react' import { useState } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { debounce } from 'lodash'
import { Comment } from 'common/comment' import { Comment } from 'common/comment'
import { User } from 'common/user' import { User } from 'common/user'
@ -9,8 +8,10 @@ import { transact } from 'web/lib/firebase/api'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { TipButton } from './contract/tip-button' import { TipButton } from './contract/tip-button'
import { Row } from './layout/row' import { Row } from './layout/row'
import { LIKE_TIP_AMOUNT } from 'common/like' import { LIKE_TIP_AMOUNT, TIP_UNDO_DURATION } from 'common/like'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { Button } from './button'
import clsx from 'clsx'
export function Tipper(prop: { export function Tipper(prop: {
comment: Comment comment: Comment
@ -19,24 +20,13 @@ export function Tipper(prop: {
}) { }) {
const { comment, myTip, totalTip } = prop const { comment, myTip, totalTip } = prop
// This is a temporary tipping amount before it actually gets confirmed. This is so tha we dont accidentally tip more than you have
const [tempTip, setTempTip] = useState(0)
const me = useUser() const me = useUser()
const [localTip, setLocalTip] = useState(myTip) const [saveTip] = useState(
() => async (user: User, comment: Comment, change: number) => {
// listen for user being set
const initialized = useRef(false)
useEffect(() => {
if (myTip && !initialized.current) {
setLocalTip(myTip)
initialized.current = true
}
}, [myTip])
const total = totalTip - myTip + localTip
// declare debounced function only on first render
const [saveTip] = useState(() =>
debounce(async (user: User, comment: Comment, change: number) => {
if (change === 0) { if (change === 0) {
return return
} }
@ -67,30 +57,88 @@ export function Tipper(prop: {
fromId: user.id, fromId: user.id,
toId: comment.userId, toId: comment.userId,
}) })
}, 1500) }
) )
// instant save on unrender
useEffect(() => () => void saveTip.flush(), [saveTip])
const addTip = (delta: number) => { const addTip = (delta: number) => {
setLocalTip(localTip + delta) setTempTip(tempTip + delta)
me && saveTip(me, comment, localTip - myTip + delta) const timeoutId = setTimeout(() => {
toast(`You tipped ${comment.userName} ${formatMoney(LIKE_TIP_AMOUNT)}!`) me &&
saveTip(me, comment, delta)
.then(() => setTempTip(tempTip - delta))
.catch((e) => console.error(e))
}, TIP_UNDO_DURATION + 1000)
toast.custom(
() => (
<TipToast
userName={comment.userName}
onUndoClick={() => {
clearTimeout(timeoutId)
setTempTip(tempTip - delta)
}}
/>
),
{ duration: TIP_UNDO_DURATION }
)
} }
const canUp = const canUp =
me && comment.userId !== me.id && me.balance >= localTip + LIKE_TIP_AMOUNT me && comment.userId !== me.id && me.balance - tempTip >= LIKE_TIP_AMOUNT
return ( return (
<Row className="items-center gap-0.5"> <Row className="items-center gap-0.5">
<TipButton <TipButton
tipAmount={LIKE_TIP_AMOUNT} tipAmount={LIKE_TIP_AMOUNT}
totalTipped={total} totalTipped={totalTip}
onClick={() => addTip(+LIKE_TIP_AMOUNT)} onClick={() => addTip(+LIKE_TIP_AMOUNT)}
userTipped={localTip > 0} userTipped={tempTip > 0 || myTip > 0}
disabled={!canUp} disabled={!canUp}
isCompact isCompact
/> />
</Row> </Row>
) )
} }
export function TipToast(props: { userName: string; onUndoClick: () => void }) {
const { userName, onUndoClick } = props
const [cancelled, setCancelled] = useState(false)
// There is a strange bug with toast where sometimes if you interact with one popup, the others will not dissappear at the right time, overriding it for now with this
const [timedOut, setTimedOut] = useState(false)
setTimeout(() => {
setTimedOut(true)
}, TIP_UNDO_DURATION)
if (timedOut) {
return <></>
}
return (
<div className="relative overflow-hidden rounded-lg bg-white drop-shadow-md">
<div
className={clsx(
'animate-progress-loading absolute bottom-0 z-10 h-1 w-full bg-indigo-600',
cancelled ? 'hidden' : ''
)}
/>
<Row className="text-greyscale-6 items-center gap-4 px-4 py-2 text-sm">
<div className={clsx(cancelled ? 'hidden' : 'inline')}>
Tipping {userName} {formatMoney(LIKE_TIP_AMOUNT)}...
</div>
<div className={clsx('py-1', cancelled ? 'inline' : 'hidden')}>
Cancelled tipping
</div>
<Button
className={clsx(cancelled ? 'hidden' : 'inline')}
size="xs"
color="gray-outline"
onClick={() => {
onUndoClick()
setCancelled(true)
}}
disabled={cancelled}
>
Cancel
</Button>
</Row>
</div>
)
}

View File

@ -0,0 +1,31 @@
import { rootCertificates } from 'tls'
export default function Coin({
size = 18,
color = '#66667C',
strokeWidth = 1.5,
fill = 'none',
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 18 18"
width={size}
height={size}
fill={fill}
stroke={color}
strokeWidth={strokeWidth}
opacity={50}
transform="rotate(-30)"
>
<path
className="cls-2"
d="M15,9c0,.35-.07,.68-.2,1-.66,1.73-3.01,3-5.8,3s-5.14-1.27-5.8-3c-.13-.32-.2-.65-.2-1,0-2.21,2.69-4,6-4s6,1.79,6,4Z"
/>
<path
className="cls-1"
d="M15,9v2c0,2.21-2.69,4-6,4s-6-1.79-6-4v-2c0,.35,.07,.68,.2,1,.66,1.73,3.01,3,5.8,3s5.14-1.27,5.8-3c.13-.32,.2-.65,.2-1Z"
/>
</svg>
)
}

View File

@ -16,6 +16,15 @@ module.exports = {
} }
), ),
extend: { extend: {
keyframes: {
progress: {
'0%': { width: '0%' },
'100%': { width: '100%' },
},
},
animation: {
'progress-loading': 'progress 2s linear',
},
colors: { colors: {
primary: '#11b981', primary: '#11b981',
'primary-focus': '#069668', 'primary-focus': '#069668',