parent
f7151f131d
commit
7a041fd753
|
@ -11,6 +11,7 @@ import { UserLink } from '../user-page'
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
contractMetrics,
|
contractMetrics,
|
||||||
|
contractPath,
|
||||||
contractPool,
|
contractPool,
|
||||||
updateContract,
|
updateContract,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
|
@ -33,6 +34,7 @@ import { ShareIconButton } from 'web/components/share-icon-button'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { Editor } from '@tiptap/react'
|
import { Editor } from '@tiptap/react'
|
||||||
import { exhibitExts } from 'common/util/parse'
|
import { exhibitExts } from 'common/util/parse'
|
||||||
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
|
||||||
export type ShowTime = 'resolve-date' | 'close-date'
|
export type ShowTime = 'resolve-date' | 'close-date'
|
||||||
|
|
||||||
|
@ -222,9 +224,12 @@ export function ContractDetails(props: {
|
||||||
<div className="whitespace-nowrap">{volumeLabel}</div>
|
<div className="whitespace-nowrap">{volumeLabel}</div>
|
||||||
</Row>
|
</Row>
|
||||||
<ShareIconButton
|
<ShareIconButton
|
||||||
contract={contract}
|
copyPayload={`https://${ENV_CONFIG.domain}${contractPath(contract)}${
|
||||||
|
user?.username && contract.creatorUsername !== user?.username
|
||||||
|
? '?referrer=' + user?.username
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
toastClassName={'sm:-left-40 -left-24 min-w-[250%]'}
|
toastClassName={'sm:-left-40 -left-24 min-w-[250%]'}
|
||||||
username={user?.username}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!disabled && <ContractInfoDialog contract={contract} bets={bets} />}
|
{!disabled && <ContractInfoDialog contract={contract} bets={bets} />}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { InfoTooltip } from '../info-tooltip'
|
||||||
import { DuplicateContractButton } from '../copy-contract-button'
|
import { DuplicateContractButton } from '../copy-contract-button'
|
||||||
|
|
||||||
export const contractDetailsButtonClassName =
|
export const contractDetailsButtonClassName =
|
||||||
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
||||||
|
|
||||||
export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
const { contract, bets } = props
|
const { contract, bets } = props
|
||||||
|
|
|
@ -3,9 +3,13 @@ import { formatMoney } from 'common/util/format'
|
||||||
import { fromNow } from 'web/lib/util/time'
|
import { fromNow } from 'web/lib/util/time'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { User } from 'web/lib/firebase/users'
|
import { Claim, Manalink } from 'common/manalink'
|
||||||
import { Button } from './button'
|
import { useState } from 'react'
|
||||||
|
import { ShareIconButton } from './share-icon-button'
|
||||||
|
import { DotsHorizontalIcon } from '@heroicons/react/solid'
|
||||||
|
import { contractDetailsButtonClassName } from './contract/contract-info-dialog'
|
||||||
|
import { useUserById } from 'web/hooks/use-user'
|
||||||
|
import getManalinkUrl from 'web/get-manalink-url'
|
||||||
export type ManalinkInfo = {
|
export type ManalinkInfo = {
|
||||||
expiresTime: number | null
|
expiresTime: number | null
|
||||||
maxUses: number | null
|
maxUses: number | null
|
||||||
|
@ -15,94 +19,202 @@ export type ManalinkInfo = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ManalinkCard(props: {
|
export function ManalinkCard(props: {
|
||||||
user: User | null | undefined
|
|
||||||
className?: string
|
|
||||||
info: ManalinkInfo
|
info: ManalinkInfo
|
||||||
isClaiming: boolean
|
className?: string
|
||||||
onClaim?: () => void
|
preview?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { user, className, isClaiming, info, onClaim } = props
|
const { className, info, preview = false } = props
|
||||||
const { expiresTime, maxUses, uses, amount, message } = info
|
const { expiresTime, maxUses, uses, amount, message } = info
|
||||||
return (
|
return (
|
||||||
<div
|
<Col>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
'min-h-20 group flex flex-col rounded-xl bg-gradient-to-br shadow-lg transition-all',
|
||||||
|
getManalinkGradient(info.amount)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Col className="mx-4 mt-2 -mb-4 text-right text-sm text-gray-100">
|
||||||
|
<div>
|
||||||
|
{maxUses != null
|
||||||
|
? `${maxUses - uses}/${maxUses} uses left`
|
||||||
|
: `Unlimited use`}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{expiresTime != null
|
||||||
|
? `Expires ${fromNow(expiresTime)}`
|
||||||
|
: 'Never expires'}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<img
|
||||||
|
className={clsx(
|
||||||
|
'block h-1/3 w-1/3 self-center transition-all group-hover:rotate-12',
|
||||||
|
preview ? 'my-2' : 'w-1/2 md:mb-6 md:h-1/2'
|
||||||
|
)}
|
||||||
|
src="/logo-white.svg"
|
||||||
|
/>
|
||||||
|
<Row className="rounded-b-xl bg-white p-4">
|
||||||
|
<Col>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'mb-1 text-xl text-indigo-500',
|
||||||
|
getManalinkAmountColor(amount)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatMoney(amount)}
|
||||||
|
</div>
|
||||||
|
<div>{message}</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ManalinkCardFromView(props: {
|
||||||
|
className?: string
|
||||||
|
link: Manalink
|
||||||
|
highlightedSlug: string
|
||||||
|
}) {
|
||||||
|
const { className, link, highlightedSlug } = props
|
||||||
|
const { message, amount, expiresTime, maxUses, claims } = link
|
||||||
|
const [details, setDetails] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
'group z-10 rounded-lg drop-shadow-sm transition-all hover:drop-shadow-lg',
|
||||||
className,
|
className,
|
||||||
'min-h-20 group flex flex-col rounded-xl bg-gradient-to-br from-indigo-200 via-indigo-400 to-indigo-800 shadow-lg transition-all'
|
link.slug === highlightedSlug ? 'animate-pulse' : ''
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Col className="mx-4 mt-2 -mb-4 text-right text-sm text-gray-100">
|
<div
|
||||||
<div>
|
className={clsx(
|
||||||
{maxUses != null
|
'relative flex flex-col rounded-t-lg bg-gradient-to-br transition-all',
|
||||||
? `${maxUses - uses}/${maxUses} uses left`
|
getManalinkGradient(link.amount)
|
||||||
: `Unlimited use`}
|
)}
|
||||||
</div>
|
onClick={() => setDetails(!details)}
|
||||||
<div>
|
>
|
||||||
{expiresTime != null
|
{details && (
|
||||||
? `Expires ${fromNow(expiresTime)}`
|
<ClaimsList
|
||||||
: 'Never expires'}
|
className="absolute h-full w-full bg-white opacity-90"
|
||||||
</div>
|
link={link}
|
||||||
</Col>
|
/>
|
||||||
|
)}
|
||||||
<img
|
<Col className="mx-4 mt-2 -mb-4 text-right text-xs text-gray-100">
|
||||||
className="mb-6 block self-center transition-all group-hover:rotate-12"
|
<div>
|
||||||
src="/logo-white.svg"
|
{maxUses != null
|
||||||
width={200}
|
? `${maxUses - claims.length}/${maxUses} uses left`
|
||||||
height={200}
|
: `Unlimited use`}
|
||||||
/>
|
</div>
|
||||||
<Row className="justify-end rounded-b-xl bg-white p-4">
|
<div>
|
||||||
<Col>
|
{expiresTime != null
|
||||||
<div className="mb-1 text-xl text-indigo-500">
|
? `Expires ${fromNow(expiresTime)}`
|
||||||
|
: 'Never expires'}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<img
|
||||||
|
className={clsx('my-auto block w-1/3 select-none self-center py-3')}
|
||||||
|
src="/logo-white.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Col className="w-full rounded-b-lg bg-white px-4 py-2 text-lg">
|
||||||
|
<Row className="relative gap-1">
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'my-auto mb-1 w-full',
|
||||||
|
getManalinkAmountColor(amount)
|
||||||
|
)}
|
||||||
|
>
|
||||||
{formatMoney(amount)}
|
{formatMoney(amount)}
|
||||||
</div>
|
</div>
|
||||||
<div>{message}</div>
|
<ShareIconButton
|
||||||
</Col>
|
toastClassName={'-left-48 min-w-[250%]'}
|
||||||
|
buttonClassName={'transition-colors'}
|
||||||
<div className="ml-auto">
|
onCopyButtonClassName={
|
||||||
<Button onClick={onClaim} disabled={isClaiming}>
|
'bg-gray-200 text-gray-600 transition-none hover:bg-gray-200 hover:text-gray-600'
|
||||||
{user ? 'Claim' : 'Login'}
|
}
|
||||||
</Button>
|
copyPayload={getManalinkUrl(link.slug)}
|
||||||
</div>
|
/>
|
||||||
</Row>
|
<button
|
||||||
</div>
|
onClick={() => setDetails(!details)}
|
||||||
|
className={clsx(
|
||||||
|
contractDetailsButtonClassName,
|
||||||
|
details
|
||||||
|
? 'bg-gray-200 text-gray-600 hover:bg-gray-200 hover:text-gray-600'
|
||||||
|
: ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DotsHorizontalIcon className="h-[24px] w-5" />
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
|
<div className="my-2 text-xs md:text-sm">{message || '\n\n'}</div>
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ManalinkCardPreview(props: {
|
function ClaimsList(props: { link: Manalink; className: string }) {
|
||||||
className?: string
|
const { link, className } = props
|
||||||
info: ManalinkInfo
|
|
||||||
}) {
|
|
||||||
const { className, info } = props
|
|
||||||
const { expiresTime, maxUses, uses, amount, message } = info
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className={clsx(
|
<Col className={clsx('px-4 py-2', className)}>
|
||||||
className,
|
<div className="text-md mb-1 mt-2 w-full font-semibold">
|
||||||
' group flex flex-col rounded-lg bg-gradient-to-br from-indigo-200 via-indigo-400 to-indigo-800 shadow-lg transition-all'
|
Claimed by...
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Col className="mx-4 mt-2 -mb-4 text-right text-xs text-gray-100">
|
|
||||||
<div>
|
|
||||||
{maxUses != null
|
|
||||||
? `${maxUses - uses}/${maxUses} uses left`
|
|
||||||
: `Unlimited use`}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="overflow-auto">
|
||||||
{expiresTime != null
|
{link.claims.length > 0 ? (
|
||||||
? `Expires ${fromNow(expiresTime)}`
|
<>
|
||||||
: 'Never expires'}
|
{link.claims.map((claim) => (
|
||||||
|
<Row key={claim.txnId}>
|
||||||
|
<Claim claim={claim} />
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="h-full">
|
||||||
|
No one has claimed this manalink yet! Share your manalink to start
|
||||||
|
spreading the wealth.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
</>
|
||||||
<img
|
|
||||||
className="my-2 block h-1/3 w-1/3 self-center transition-all group-hover:rotate-12"
|
|
||||||
src="/logo-white.svg"
|
|
||||||
/>
|
|
||||||
<Row className="rounded-b-lg bg-white p-2">
|
|
||||||
<Col className="text-md">
|
|
||||||
<div className="mb-1 text-indigo-500">{formatMoney(amount)}</div>
|
|
||||||
<div className="text-xs">{message}</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Claim(props: { claim: Claim }) {
|
||||||
|
const { claim } = props
|
||||||
|
const who = useUserById(claim.toId)
|
||||||
|
return (
|
||||||
|
<Row className="my-1 gap-2 text-xs">
|
||||||
|
<div>{who?.name || 'Loading...'}</div>
|
||||||
|
<div className="text-gray-500">{fromNow(claim.claimedTime)}</div>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManalinkGradient(amount: number) {
|
||||||
|
if (amount < 20) {
|
||||||
|
return 'from-indigo-200 via-indigo-500 to-indigo-800'
|
||||||
|
} else if (amount >= 20 && amount < 50) {
|
||||||
|
return 'from-fuchsia-200 via-fuchsia-500 to-fuchsia-800'
|
||||||
|
} else if (amount >= 50 && amount < 100) {
|
||||||
|
return 'from-rose-100 via-rose-400 to-rose-700'
|
||||||
|
} else if (amount >= 100) {
|
||||||
|
return 'from-amber-200 via-amber-500 to-amber-700'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManalinkAmountColor(amount: number) {
|
||||||
|
if (amount < 20) {
|
||||||
|
return 'text-indigo-500'
|
||||||
|
} else if (amount >= 20 && amount < 50) {
|
||||||
|
return 'text-fuchsia-600'
|
||||||
|
} else if (amount >= 50 && amount < 100) {
|
||||||
|
return 'text-rose-600'
|
||||||
|
} else if (amount >= 100) {
|
||||||
|
return 'text-amber-600'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Col } from '../layout/col'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { Title } from '../title'
|
import { Title } from '../title'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { ManalinkCardPreview, ManalinkInfo } from 'web/components/manalink-card'
|
import { ManalinkCard, ManalinkInfo } from 'web/components/manalink-card'
|
||||||
import { createManalink } from 'web/lib/firebase/manalinks'
|
import { createManalink } from 'web/lib/firebase/manalinks'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
import Textarea from 'react-expanding-textarea'
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
@ -37,6 +37,7 @@ export function CreateLinksButton(props: {
|
||||||
message: newManalink.message,
|
message: newManalink.message,
|
||||||
})
|
})
|
||||||
setHighlightedSlug(slug || '')
|
setHighlightedSlug(slug || '')
|
||||||
|
setTimeout(() => setHighlightedSlug(''), 3700)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -191,7 +192,7 @@ function CreateManalinkForm(props: {
|
||||||
{finishedCreating && (
|
{finishedCreating && (
|
||||||
<>
|
<>
|
||||||
<Title className="!my-0" text="Manalink Created!" />
|
<Title className="!my-0" text="Manalink Created!" />
|
||||||
<ManalinkCardPreview className="my-4" info={newManalink} />
|
<ManalinkCard className="my-4" info={newManalink} preview />
|
||||||
<Row
|
<Row
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded border bg-gray-50 py-2 px-3 text-sm text-gray-500 transition-colors duration-700',
|
'rounded border bg-gray-50 py-2 px-3 text-sm text-gray-500 transition-colors duration-700',
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
export function Pagination(props: {
|
export function Pagination(props: {
|
||||||
page: number
|
page: number
|
||||||
itemsPerPage: number
|
itemsPerPage: number
|
||||||
totalItems: number
|
totalItems: number
|
||||||
setPage: (page: number) => void
|
setPage: (page: number) => void
|
||||||
scrollToTop?: boolean
|
scrollToTop?: boolean
|
||||||
|
className?: string
|
||||||
nextTitle?: string
|
nextTitle?: string
|
||||||
prevTitle?: string
|
prevTitle?: string
|
||||||
}) {
|
}) {
|
||||||
|
@ -15,13 +18,17 @@ export function Pagination(props: {
|
||||||
scrollToTop,
|
scrollToTop,
|
||||||
nextTitle,
|
nextTitle,
|
||||||
prevTitle,
|
prevTitle,
|
||||||
|
className,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const maxPage = Math.ceil(totalItems / itemsPerPage) - 1
|
const maxPage = Math.ceil(totalItems / itemsPerPage) - 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6"
|
className={clsx(
|
||||||
|
'flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6',
|
||||||
|
className
|
||||||
|
)}
|
||||||
aria-label="Pagination"
|
aria-label="Pagination"
|
||||||
>
|
>
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">
|
||||||
|
|
|
@ -2,65 +2,48 @@ import React, { useState } from 'react'
|
||||||
import { ShareIcon } from '@heroicons/react/outline'
|
import { ShareIcon } from '@heroicons/react/outline'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
|
||||||
import { copyToClipboard } from 'web/lib/util/copy'
|
import { copyToClipboard } from 'web/lib/util/copy'
|
||||||
import { contractPath } from 'web/lib/firebase/contracts'
|
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
|
||||||
import { ToastClipboard } from 'web/components/toast-clipboard'
|
import { ToastClipboard } from 'web/components/toast-clipboard'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { contractDetailsButtonClassName } from 'web/components/contract/contract-info-dialog'
|
import { contractDetailsButtonClassName } from 'web/components/contract/contract-info-dialog'
|
||||||
import { Group } from 'common/group'
|
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
|
||||||
|
|
||||||
function copyContractWithReferral(contract: Contract, username?: string) {
|
|
||||||
const postFix =
|
|
||||||
username && contract.creatorUsername !== username
|
|
||||||
? '?referrer=' + username
|
|
||||||
: ''
|
|
||||||
copyToClipboard(
|
|
||||||
`https://${ENV_CONFIG.domain}${contractPath(contract)}${postFix}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: if a user arrives at a /group endpoint with a ?referral= query, they'll be added to the group automatically
|
|
||||||
function copyGroupWithReferral(group: Group, username?: string) {
|
|
||||||
const postFix = username ? '?referrer=' + username : ''
|
|
||||||
copyToClipboard(
|
|
||||||
`https://${ENV_CONFIG.domain}${groupPath(group.slug)}${postFix}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShareIconButton(props: {
|
export function ShareIconButton(props: {
|
||||||
contract?: Contract
|
|
||||||
group?: Group
|
|
||||||
buttonClassName?: string
|
buttonClassName?: string
|
||||||
|
onCopyButtonClassName?: string
|
||||||
toastClassName?: string
|
toastClassName?: string
|
||||||
username?: string
|
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
|
iconClassName?: string
|
||||||
|
copyPayload: string
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
contract,
|
|
||||||
buttonClassName,
|
buttonClassName,
|
||||||
|
onCopyButtonClassName,
|
||||||
toastClassName,
|
toastClassName,
|
||||||
username,
|
|
||||||
group,
|
|
||||||
children,
|
children,
|
||||||
|
iconClassName,
|
||||||
|
copyPayload,
|
||||||
} = props
|
} = props
|
||||||
const [showToast, setShowToast] = useState(false)
|
const [showToast, setShowToast] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex-shrink-0">
|
<div className="relative z-10 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
className={clsx(contractDetailsButtonClassName, buttonClassName)}
|
className={clsx(
|
||||||
|
contractDetailsButtonClassName,
|
||||||
|
buttonClassName,
|
||||||
|
showToast ? onCopyButtonClassName : ''
|
||||||
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (contract) copyContractWithReferral(contract, username)
|
copyToClipboard(copyPayload)
|
||||||
if (group) copyGroupWithReferral(group, username)
|
|
||||||
track('copy share link')
|
track('copy share link')
|
||||||
setShowToast(true)
|
setShowToast(true)
|
||||||
setTimeout(() => setShowToast(false), 2000)
|
setTimeout(() => setShowToast(false), 2000)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ShareIcon className="h-[24px] w-5" aria-hidden="true" />
|
<ShareIcon
|
||||||
|
className={clsx(iconClassName ? iconClassName : 'h-[24px] w-5')}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ import { useManalink } from 'web/lib/firebase/manalinks'
|
||||||
import { ManalinkCard } from 'web/components/manalink-card'
|
import { ManalinkCard } from 'web/components/manalink-card'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||||
|
import { Row } from 'web/components/layout/row'
|
||||||
|
import { Button } from 'web/components/button'
|
||||||
|
|
||||||
export default function ClaimPage() {
|
export default function ClaimPage() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -28,34 +30,42 @@ export default function ClaimPage() {
|
||||||
description="Send mana to anyone via link!"
|
description="Send mana to anyone via link!"
|
||||||
url="/send"
|
url="/send"
|
||||||
/>
|
/>
|
||||||
<div className="mx-auto max-w-xl">
|
<div className="mx-auto max-w-xl px-2">
|
||||||
<Title text={`Claim M$${manalink.amount} mana`} />
|
<Row className="items-center justify-between">
|
||||||
<ManalinkCard
|
<Title text={`Claim M$${manalink.amount} mana`} />
|
||||||
user={user}
|
<div className="my-auto">
|
||||||
info={info}
|
<Button
|
||||||
isClaiming={claiming}
|
onClick={async () => {
|
||||||
onClaim={async () => {
|
setClaiming(true)
|
||||||
setClaiming(true)
|
try {
|
||||||
try {
|
if (user == null) {
|
||||||
if (user == null) {
|
await firebaseLogin()
|
||||||
await firebaseLogin()
|
setClaiming(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (user?.id == manalink.fromId) {
|
||||||
|
throw new Error("You can't claim your own manalink.")
|
||||||
|
}
|
||||||
|
await claimManalink({ slug: manalink.slug })
|
||||||
|
user && router.push(`/${user.username}?claimed-mana=yes`)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
const message =
|
||||||
|
e && e instanceof Object
|
||||||
|
? e.toString()
|
||||||
|
: 'An error occurred.'
|
||||||
|
setError(message)
|
||||||
|
}
|
||||||
setClaiming(false)
|
setClaiming(false)
|
||||||
return
|
}}
|
||||||
}
|
disabled={claiming}
|
||||||
if (user?.id == manalink.fromId) {
|
size="lg"
|
||||||
throw new Error("You can't claim your own manalink.")
|
>
|
||||||
}
|
{user ? 'Claim' : 'Login'}
|
||||||
await claimManalink({ slug: manalink.slug })
|
</Button>
|
||||||
user && router.push(`/${user.username}?claimed-mana=yes`)
|
</div>
|
||||||
} catch (e) {
|
</Row>
|
||||||
console.log(e)
|
<ManalinkCard info={info} />
|
||||||
const message =
|
|
||||||
e && e instanceof Object ? e.toString() : 'An error occurred.'
|
|
||||||
setError(message)
|
|
||||||
}
|
|
||||||
setClaiming(false)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{error && (
|
{error && (
|
||||||
<section className="my-5 text-red-500">
|
<section className="my-5 text-red-500">
|
||||||
<p>Failed to claim manalink.</p>
|
<p>Failed to claim manalink.</p>
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import clsx from 'clsx'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
|
|
||||||
import { Claim, Manalink } from 'common/manalink'
|
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
|
@ -11,7 +8,6 @@ import { Title } from 'web/components/title'
|
||||||
import { Subtitle } from 'web/components/subtitle'
|
import { Subtitle } from 'web/components/subtitle'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { useUserManalinks } from 'web/lib/firebase/manalinks'
|
import { useUserManalinks } from 'web/lib/firebase/manalinks'
|
||||||
import { fromNow } from 'web/lib/util/time'
|
|
||||||
import { useUserById } from 'web/hooks/use-user'
|
import { useUserById } from 'web/hooks/use-user'
|
||||||
import { ManalinkTxn } from 'common/txn'
|
import { ManalinkTxn } from 'common/txn'
|
||||||
import { Avatar } from 'web/components/avatar'
|
import { Avatar } from 'web/components/avatar'
|
||||||
|
@ -22,8 +18,11 @@ import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
||||||
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||||
|
import { ManalinkCardFromView } from 'web/components/manalink-card'
|
||||||
|
import { Pagination } from 'web/components/pagination'
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
|
const LINKS_PER_PAGE = 24
|
||||||
export const getServerSideProps = redirectIfLoggedOut('/')
|
export const getServerSideProps = redirectIfLoggedOut('/')
|
||||||
|
|
||||||
export function getManalinkUrl(slug: string) {
|
export function getManalinkUrl(slug: string) {
|
||||||
|
@ -40,6 +39,10 @@ export default function LinkPage() {
|
||||||
(l.maxUses == null || l.claimedUserIds.length < l.maxUses) &&
|
(l.maxUses == null || l.claimedUserIds.length < l.maxUses) &&
|
||||||
(l.expiresTime == null || l.expiresTime > Date.now())
|
(l.expiresTime == null || l.expiresTime > Date.now())
|
||||||
)
|
)
|
||||||
|
const [page, setPage] = useState(0)
|
||||||
|
const start = page * LINKS_PER_PAGE
|
||||||
|
const end = start + LINKS_PER_PAGE
|
||||||
|
const displayedLinks = unclaimedLinks.slice(start, end)
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return null
|
return null
|
||||||
|
@ -68,12 +71,30 @@ export default function LinkPage() {
|
||||||
don't yet have a Manifold account.
|
don't yet have a Manifold account.
|
||||||
</p>
|
</p>
|
||||||
<Subtitle text="Your Manalinks" />
|
<Subtitle text="Your Manalinks" />
|
||||||
<LinksTable links={unclaimedLinks} highlightedSlug={highlightedSlug} />
|
<Col className="grid w-full gap-4 md:grid-cols-2">
|
||||||
|
{displayedLinks.map((link) => {
|
||||||
|
return (
|
||||||
|
<ManalinkCardFromView
|
||||||
|
link={link}
|
||||||
|
highlightedSlug={highlightedSlug}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Col>
|
||||||
|
<Pagination
|
||||||
|
page={page}
|
||||||
|
itemsPerPage={LINKS_PER_PAGE}
|
||||||
|
totalItems={unclaimedLinks.length}
|
||||||
|
setPage={setPage}
|
||||||
|
className="mt-4 bg-transparent"
|
||||||
|
scrollToTop
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: either utilize this or get rid of it
|
||||||
export function ClaimsList(props: { txns: ManalinkTxn[] }) {
|
export function ClaimsList(props: { txns: ManalinkTxn[] }) {
|
||||||
const { txns } = props
|
const { txns } = props
|
||||||
return (
|
return (
|
||||||
|
@ -121,127 +142,3 @@ export function ClaimDescription(props: { txn: ManalinkTxn }) {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClaimTableRow(props: { claim: Claim }) {
|
|
||||||
const { claim } = props
|
|
||||||
const who = useUserById(claim.toId)
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td className="px-5 py-2">{who?.name || 'Loading...'}</td>
|
|
||||||
<td className="px-5 py-2">{`${new Date(
|
|
||||||
claim.claimedTime
|
|
||||||
).toLocaleString()}, ${fromNow(claim.claimedTime)}`}</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LinkDetailsTable(props: { link: Manalink }) {
|
|
||||||
const { link } = props
|
|
||||||
return (
|
|
||||||
<table className="w-full divide-y divide-gray-300 border border-gray-400">
|
|
||||||
<thead className="bg-gray-50 text-left text-sm font-semibold text-gray-900">
|
|
||||||
<tr>
|
|
||||||
<th className="px-5 py-2">Claimed by</th>
|
|
||||||
<th className="px-5 py-2">Time</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200 bg-white text-sm text-gray-500">
|
|
||||||
{link.claims.length ? (
|
|
||||||
link.claims.map((claim) => <ClaimTableRow claim={claim} />)
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td className="px-5 py-2" colSpan={2}>
|
|
||||||
No claims yet.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LinkTableRow(props: { link: Manalink; highlight: boolean }) {
|
|
||||||
const { link, highlight } = props
|
|
||||||
const [expanded, setExpanded] = useState(false)
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LinkSummaryRow
|
|
||||||
link={link}
|
|
||||||
highlight={highlight}
|
|
||||||
expanded={expanded}
|
|
||||||
onToggle={() => setExpanded((exp) => !exp)}
|
|
||||||
/>
|
|
||||||
{expanded && (
|
|
||||||
<tr>
|
|
||||||
<td className="bg-gray-100 p-3" colSpan={5}>
|
|
||||||
<LinkDetailsTable link={link} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LinkSummaryRow(props: {
|
|
||||||
link: Manalink
|
|
||||||
highlight: boolean
|
|
||||||
expanded: boolean
|
|
||||||
onToggle: () => void
|
|
||||||
}) {
|
|
||||||
const { link, highlight, expanded, onToggle } = props
|
|
||||||
const className = clsx(
|
|
||||||
'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white',
|
|
||||||
highlight ? 'bg-indigo-100 rounded-lg animate-pulse' : ''
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<tr id={link.slug} key={link.slug} className={className}>
|
|
||||||
<td className="py-4 pl-5" onClick={onToggle}>
|
|
||||||
{expanded ? (
|
|
||||||
<ChevronUpIcon className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<ChevronDownIcon className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-5 py-4 font-medium text-gray-900">
|
|
||||||
{formatMoney(link.amount)}
|
|
||||||
</td>
|
|
||||||
<td className="px-5 py-4">{getManalinkUrl(link.slug)}</td>
|
|
||||||
<td className="px-5 py-4">{link.claimedUserIds.length}</td>
|
|
||||||
<td className="px-5 py-4">{link.maxUses == null ? '∞' : link.maxUses}</td>
|
|
||||||
<td className="px-5 py-4">
|
|
||||||
{link.expiresTime == null ? 'Never' : fromNow(link.expiresTime)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LinksTable(props: { links: Manalink[]; highlightedSlug?: string }) {
|
|
||||||
const { links, highlightedSlug } = props
|
|
||||||
return links.length == 0 ? (
|
|
||||||
<p>You don't currently have any outstanding manalinks.</p>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-scroll">
|
|
||||||
<table className="w-full divide-y divide-gray-300 rounded-lg border border-gray-200">
|
|
||||||
<thead className="bg-gray-50 text-left text-sm font-semibold text-gray-900">
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th className="px-5 py-3.5">Amount</th>
|
|
||||||
<th className="px-5 py-3.5">Link</th>
|
|
||||||
<th className="px-5 py-3.5">Uses</th>
|
|
||||||
<th className="px-5 py-3.5">Max Uses</th>
|
|
||||||
<th className="px-5 py-3.5">Expires</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200 bg-white">
|
|
||||||
{links.map((link) => (
|
|
||||||
<LinkTableRow
|
|
||||||
link={link}
|
|
||||||
highlight={link.slug === highlightedSlug}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user