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:
parent
3f8988bf27
commit
4359ad0530
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
31
web/public/custom-components/coin.tsx
Normal file
31
web/public/custom-components/coin.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user