Merge branch 'main' into range-order
This commit is contained in:
commit
5dead681b3
|
@ -3,4 +3,4 @@ export const NUMERIC_FIXED_VAR = 0.005
|
||||||
|
|
||||||
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
||||||
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
||||||
export const UNIQUE_BETTOR_BONUS_AMOUNT = 5
|
export const UNIQUE_BETTOR_BONUS_AMOUNT = 10
|
||||||
|
|
|
@ -29,12 +29,22 @@ export const createNotification = async (
|
||||||
sourceUser: User,
|
sourceUser: User,
|
||||||
idempotencyKey: string,
|
idempotencyKey: string,
|
||||||
sourceText: string,
|
sourceText: string,
|
||||||
sourceContract?: Contract,
|
miscData?: {
|
||||||
relatedSourceType?: notification_source_types,
|
contract?: Contract
|
||||||
relatedUserId?: string,
|
relatedSourceType?: notification_source_types
|
||||||
sourceSlug?: string,
|
relatedUserId?: string
|
||||||
sourceTitle?: string
|
slug?: string
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
) => {
|
) => {
|
||||||
|
const {
|
||||||
|
contract: sourceContract,
|
||||||
|
relatedSourceType,
|
||||||
|
relatedUserId,
|
||||||
|
slug,
|
||||||
|
title,
|
||||||
|
} = miscData ?? {}
|
||||||
|
|
||||||
const shouldGetNotification = (
|
const shouldGetNotification = (
|
||||||
userId: string,
|
userId: string,
|
||||||
userToReasonTexts: user_to_reason_texts
|
userToReasonTexts: user_to_reason_texts
|
||||||
|
@ -70,8 +80,8 @@ export const createNotification = async (
|
||||||
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
||||||
sourceContractTitle: sourceContract?.question,
|
sourceContractTitle: sourceContract?.question,
|
||||||
sourceContractSlug: sourceContract?.slug,
|
sourceContractSlug: sourceContract?.slug,
|
||||||
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
|
sourceSlug: slug ? slug : sourceContract?.slug,
|
||||||
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
|
sourceTitle: title ? title : sourceContract?.question,
|
||||||
}
|
}
|
||||||
await notificationRef.set(removeUndefinedProps(notification))
|
await notificationRef.set(removeUndefinedProps(notification))
|
||||||
})
|
})
|
||||||
|
|
|
@ -64,7 +64,7 @@ async function sendMarketCloseEmails() {
|
||||||
user,
|
user,
|
||||||
'closed' + contract.id.slice(6, contract.id.length),
|
'closed' + contract.id.slice(6, contract.id.length),
|
||||||
contract.closeTime?.toString() ?? new Date().toString(),
|
contract.closeTime?.toString() ?? new Date().toString(),
|
||||||
contract
|
{ contract }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,6 @@ export const onCreateAnswer = functions.firestore
|
||||||
answerCreator,
|
answerCreator,
|
||||||
eventId,
|
eventId,
|
||||||
answer.text,
|
answer.text,
|
||||||
contract
|
{ contract }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -134,12 +134,11 @@ const updateUniqueBettorsAndGiveCreatorBonus = async (
|
||||||
fromUser,
|
fromUser,
|
||||||
eventId + '-bonus',
|
eventId + '-bonus',
|
||||||
result.txn.amount + '',
|
result.txn.amount + '',
|
||||||
contract,
|
{
|
||||||
undefined,
|
contract,
|
||||||
// No need to set the user id, we'll use the contract creator id
|
slug: contract.slug,
|
||||||
undefined,
|
title: contract.question,
|
||||||
contract.slug,
|
}
|
||||||
contract.question
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ export const onCreateCommentOnContract = functions
|
||||||
? 'answer'
|
? 'answer'
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const relatedUser = comment.replyToCommentId
|
const relatedUserId = comment.replyToCommentId
|
||||||
? comments.find((c) => c.id === comment.replyToCommentId)?.userId
|
? comments.find((c) => c.id === comment.replyToCommentId)?.userId
|
||||||
: answer?.userId
|
: answer?.userId
|
||||||
|
|
||||||
|
@ -79,9 +79,7 @@ export const onCreateCommentOnContract = functions
|
||||||
commentCreator,
|
commentCreator,
|
||||||
eventId,
|
eventId,
|
||||||
comment.text,
|
comment.text,
|
||||||
contract,
|
{ contract, relatedSourceType, relatedUserId }
|
||||||
relatedSourceType,
|
|
||||||
relatedUser
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const recipientUserIds = uniq([
|
const recipientUserIds = uniq([
|
||||||
|
|
|
@ -21,6 +21,6 @@ export const onCreateContract = functions.firestore
|
||||||
contractCreator,
|
contractCreator,
|
||||||
eventId,
|
eventId,
|
||||||
richTextToString(contract.description as JSONContent),
|
richTextToString(contract.description as JSONContent),
|
||||||
contract
|
{ contract }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,11 +20,11 @@ export const onCreateGroup = functions.firestore
|
||||||
groupCreator,
|
groupCreator,
|
||||||
eventId,
|
eventId,
|
||||||
group.about,
|
group.about,
|
||||||
undefined,
|
{
|
||||||
undefined,
|
relatedUserId: memberId,
|
||||||
memberId,
|
slug: group.slug,
|
||||||
group.slug,
|
title: group.name,
|
||||||
group.name
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -26,6 +26,6 @@ export const onCreateLiquidityProvision = functions.firestore
|
||||||
liquidityProvider,
|
liquidityProvider,
|
||||||
eventId,
|
eventId,
|
||||||
liquidity.amount.toString(),
|
liquidity.amount.toString(),
|
||||||
contract
|
{ contract }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -30,9 +30,7 @@ export const onFollowUser = functions.firestore
|
||||||
followingUser,
|
followingUser,
|
||||||
eventId,
|
eventId,
|
||||||
'',
|
'',
|
||||||
undefined,
|
{ relatedUserId: follow.userId }
|
||||||
undefined,
|
|
||||||
follow.userId
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ export const onUpdateContract = functions.firestore
|
||||||
contractUpdater,
|
contractUpdater,
|
||||||
eventId,
|
eventId,
|
||||||
resolutionText,
|
resolutionText,
|
||||||
contract
|
{ contract }
|
||||||
)
|
)
|
||||||
} else if (
|
} else if (
|
||||||
previousValue.closeTime !== contract.closeTime ||
|
previousValue.closeTime !== contract.closeTime ||
|
||||||
|
@ -62,7 +62,7 @@ export const onUpdateContract = functions.firestore
|
||||||
contractUpdater,
|
contractUpdater,
|
||||||
eventId,
|
eventId,
|
||||||
sourceText,
|
sourceText,
|
||||||
contract
|
{ contract }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
# Ignore Next artifacts
|
# Ignore Next artifacts
|
||||||
.next/
|
.next/
|
||||||
out/
|
out/
|
||||||
|
public/**/*.json
|
77
web/components/auth-context.tsx
Normal file
77
web/components/auth-context.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { createContext, useEffect } from 'react'
|
||||||
|
import { User } from 'common/user'
|
||||||
|
import { onIdTokenChanged } from 'firebase/auth'
|
||||||
|
import {
|
||||||
|
auth,
|
||||||
|
listenForUser,
|
||||||
|
getUser,
|
||||||
|
setCachedReferralInfoForUser,
|
||||||
|
} from 'web/lib/firebase/users'
|
||||||
|
import { deleteAuthCookies, setAuthCookies } from 'web/lib/firebase/auth'
|
||||||
|
import { createUser } from 'web/lib/firebase/api'
|
||||||
|
import { randomString } from 'common/util/random'
|
||||||
|
import { identifyUser, setUserProperty } from 'web/lib/service/analytics'
|
||||||
|
import { useStateCheckEquality } from 'web/hooks/use-state-check-equality'
|
||||||
|
|
||||||
|
// Either we haven't looked up the logged in user yet (undefined), or we know
|
||||||
|
// the user is not logged in (null), or we know the user is logged in (User).
|
||||||
|
type AuthUser = undefined | null | User
|
||||||
|
|
||||||
|
const CACHED_USER_KEY = 'CACHED_USER_KEY'
|
||||||
|
|
||||||
|
const ensureDeviceToken = () => {
|
||||||
|
let deviceToken = localStorage.getItem('device-token')
|
||||||
|
if (!deviceToken) {
|
||||||
|
deviceToken = randomString()
|
||||||
|
localStorage.setItem('device-token', deviceToken)
|
||||||
|
}
|
||||||
|
return deviceToken
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthContext = createContext<AuthUser>(null)
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: any) {
|
||||||
|
const [authUser, setAuthUser] = useStateCheckEquality<AuthUser>(undefined)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cachedUser = localStorage.getItem(CACHED_USER_KEY)
|
||||||
|
setAuthUser(cachedUser && JSON.parse(cachedUser))
|
||||||
|
}, [setAuthUser])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return onIdTokenChanged(auth, async (fbUser) => {
|
||||||
|
if (fbUser) {
|
||||||
|
let user = await getUser(fbUser.uid)
|
||||||
|
if (!user) {
|
||||||
|
const deviceToken = ensureDeviceToken()
|
||||||
|
user = (await createUser({ deviceToken })) as User
|
||||||
|
}
|
||||||
|
setAuthUser(user)
|
||||||
|
// Persist to local storage, to reduce login blink next time.
|
||||||
|
// Note: Cap on localStorage size is ~5mb
|
||||||
|
localStorage.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
||||||
|
setCachedReferralInfoForUser(user)
|
||||||
|
setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken)
|
||||||
|
} else {
|
||||||
|
// User logged out; reset to null
|
||||||
|
setAuthUser(null)
|
||||||
|
localStorage.removeItem(CACHED_USER_KEY)
|
||||||
|
deleteAuthCookies()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [setAuthUser])
|
||||||
|
|
||||||
|
const authUserId = authUser?.id
|
||||||
|
const authUsername = authUser?.username
|
||||||
|
useEffect(() => {
|
||||||
|
if (authUserId && authUsername) {
|
||||||
|
identifyUser(authUserId)
|
||||||
|
setUserProperty('username', authUsername)
|
||||||
|
return listenForUser(authUserId, setAuthUser)
|
||||||
|
}
|
||||||
|
}, [authUserId, authUsername, setAuthUser])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={authUser}>{children}</AuthContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -78,10 +78,10 @@ export function BetsList(props: {
|
||||||
|
|
||||||
const getTime = useTimeSinceFirstRender()
|
const getTime = useTimeSinceFirstRender()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bets && contractsById) {
|
if (bets && contractsById && signedInUser) {
|
||||||
trackLatency('portfolio', getTime())
|
trackLatency(signedInUser.id, 'portfolio', getTime())
|
||||||
}
|
}
|
||||||
}, [bets, contractsById, getTime])
|
}, [signedInUser, bets, contractsById, getTime])
|
||||||
|
|
||||||
if (!bets || !contractsById) {
|
if (!bets || !contractsById) {
|
||||||
return <LoadingIndicator />
|
return <LoadingIndicator />
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { Spacer } from './layout/spacer'
|
||||||
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { useFollows } from 'web/hooks/use-follows'
|
import { useFollows } from 'web/hooks/use-follows'
|
||||||
import { trackCallback } from 'web/lib/service/analytics'
|
import { track, trackCallback } from 'web/lib/service/analytics'
|
||||||
import ContractSearchFirestore from 'web/pages/contract-search-firestore'
|
import ContractSearchFirestore from 'web/pages/contract-search-firestore'
|
||||||
import { useMemberGroups } from 'web/hooks/use-group'
|
import { useMemberGroups } from 'web/hooks/use-group'
|
||||||
import { Group, NEW_USER_GROUP_SLUGS } from 'common/group'
|
import { Group, NEW_USER_GROUP_SLUGS } from 'common/group'
|
||||||
|
@ -111,8 +111,14 @@ export function ContractSearch(props: {
|
||||||
querySortOptions?.defaultFilter ?? 'open'
|
querySortOptions?.defaultFilter ?? 'open'
|
||||||
)
|
)
|
||||||
const pillsEnabled = !additionalFilter
|
const pillsEnabled = !additionalFilter
|
||||||
|
|
||||||
const [pillFilter, setPillFilter] = useState<string | undefined>(undefined)
|
const [pillFilter, setPillFilter] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
|
const selectFilter = (pill: string | undefined) => () => {
|
||||||
|
setPillFilter(pill)
|
||||||
|
track('select search category', { category: pill ?? 'all' })
|
||||||
|
}
|
||||||
|
|
||||||
const { filters, numericFilters } = useMemo(() => {
|
const { filters, numericFilters } = useMemo(() => {
|
||||||
let filters = [
|
let filters = [
|
||||||
filter === 'open' ? 'isResolved:false' : '',
|
filter === 'open' ? 'isResolved:false' : '',
|
||||||
|
@ -191,7 +197,7 @@ export function ContractSearch(props: {
|
||||||
className="!select !select-bordered"
|
className="!select !select-bordered"
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={(e) => setFilter(e.target.value as filter)}
|
onChange={(e) => setFilter(e.target.value as filter)}
|
||||||
onBlur={trackCallback('select search filter')}
|
onBlur={trackCallback('select search filter', { filter })}
|
||||||
>
|
>
|
||||||
<option value="open">Open</option>
|
<option value="open">Open</option>
|
||||||
<option value="closed">Closed</option>
|
<option value="closed">Closed</option>
|
||||||
|
@ -204,7 +210,7 @@ export function ContractSearch(props: {
|
||||||
classNames={{
|
classNames={{
|
||||||
select: '!select !select-bordered',
|
select: '!select !select-bordered',
|
||||||
}}
|
}}
|
||||||
onBlur={trackCallback('select search sort')}
|
onBlur={trackCallback('select search sort', { sort })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Configure
|
<Configure
|
||||||
|
@ -222,14 +228,14 @@ export function ContractSearch(props: {
|
||||||
<PillButton
|
<PillButton
|
||||||
key={'all'}
|
key={'all'}
|
||||||
selected={pillFilter === undefined}
|
selected={pillFilter === undefined}
|
||||||
onSelect={() => setPillFilter(undefined)}
|
onSelect={selectFilter(undefined)}
|
||||||
>
|
>
|
||||||
All
|
All
|
||||||
</PillButton>
|
</PillButton>
|
||||||
<PillButton
|
<PillButton
|
||||||
key={'personal'}
|
key={'personal'}
|
||||||
selected={pillFilter === 'personal'}
|
selected={pillFilter === 'personal'}
|
||||||
onSelect={() => setPillFilter('personal')}
|
onSelect={selectFilter('personal')}
|
||||||
>
|
>
|
||||||
For you
|
For you
|
||||||
</PillButton>
|
</PillButton>
|
||||||
|
@ -237,7 +243,7 @@ export function ContractSearch(props: {
|
||||||
<PillButton
|
<PillButton
|
||||||
key={'your-bets'}
|
key={'your-bets'}
|
||||||
selected={pillFilter === 'your-bets'}
|
selected={pillFilter === 'your-bets'}
|
||||||
onSelect={() => setPillFilter('your-bets')}
|
onSelect={selectFilter('your-bets')}
|
||||||
>
|
>
|
||||||
Your bets
|
Your bets
|
||||||
</PillButton>
|
</PillButton>
|
||||||
|
@ -247,7 +253,7 @@ export function ContractSearch(props: {
|
||||||
<PillButton
|
<PillButton
|
||||||
key={slug}
|
key={slug}
|
||||||
selected={pillFilter === slug}
|
selected={pillFilter === slug}
|
||||||
onSelect={() => setPillFilter(slug)}
|
onSelect={selectFilter(slug)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</PillButton>
|
</PillButton>
|
||||||
|
|
|
@ -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
|
||||||
|
|
141
web/components/contract/contract-leaderboard.tsx
Normal file
141
web/components/contract/contract-leaderboard.tsx
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import { Bet } from 'common/bet'
|
||||||
|
import { Comment } from 'common/comment'
|
||||||
|
import { resolvedPayout } from 'common/calculate'
|
||||||
|
import { Contract } from 'common/contract'
|
||||||
|
import { formatMoney } from 'common/util/format'
|
||||||
|
import { groupBy, mapValues, sumBy, sortBy, keyBy } from 'lodash'
|
||||||
|
import { useState, useMemo, useEffect } from 'react'
|
||||||
|
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||||
|
import { useUserById } from 'web/hooks/use-user'
|
||||||
|
import { listUsers, User } from 'web/lib/firebase/users'
|
||||||
|
import { FeedBet } from '../feed/feed-bets'
|
||||||
|
import { FeedComment } from '../feed/feed-comments'
|
||||||
|
import { Spacer } from '../layout/spacer'
|
||||||
|
import { Leaderboard } from '../leaderboard'
|
||||||
|
import { Title } from '../title'
|
||||||
|
|
||||||
|
export function ContractLeaderboard(props: {
|
||||||
|
contract: Contract
|
||||||
|
bets: Bet[]
|
||||||
|
}) {
|
||||||
|
const { contract, bets } = props
|
||||||
|
const [users, setUsers] = useState<User[]>()
|
||||||
|
|
||||||
|
const { userProfits, top5Ids } = useMemo(() => {
|
||||||
|
// Create a map of userIds to total profits (including sales)
|
||||||
|
const openBets = bets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
|
const betsByUser = groupBy(openBets, 'userId')
|
||||||
|
|
||||||
|
const userProfits = mapValues(betsByUser, (bets) =>
|
||||||
|
sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount)
|
||||||
|
)
|
||||||
|
// Find the 5 users with the most profits
|
||||||
|
const top5Ids = Object.entries(userProfits)
|
||||||
|
.sort(([_i1, p1], [_i2, p2]) => p2 - p1)
|
||||||
|
.filter(([, p]) => p > 0)
|
||||||
|
.slice(0, 5)
|
||||||
|
.map(([id]) => id)
|
||||||
|
return { userProfits, top5Ids }
|
||||||
|
}, [contract, bets])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (top5Ids.length > 0) {
|
||||||
|
listUsers(top5Ids).then((users) => {
|
||||||
|
const sortedUsers = sortBy(users, (user) => -userProfits[user.id])
|
||||||
|
setUsers(sortedUsers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [userProfits, top5Ids])
|
||||||
|
|
||||||
|
return users && users.length > 0 ? (
|
||||||
|
<Leaderboard
|
||||||
|
title="🏅 Top bettors"
|
||||||
|
users={users || []}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'Total profit',
|
||||||
|
renderCell: (user) => formatMoney(userProfits[user.id] || 0),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
className="mt-12 max-w-sm"
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContractTopTrades(props: {
|
||||||
|
contract: Contract
|
||||||
|
bets: Bet[]
|
||||||
|
comments: Comment[]
|
||||||
|
tips: CommentTipMap
|
||||||
|
}) {
|
||||||
|
const { contract, bets, comments, tips } = props
|
||||||
|
const commentsById = keyBy(comments, 'id')
|
||||||
|
const betsById = keyBy(bets, 'id')
|
||||||
|
|
||||||
|
// If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit
|
||||||
|
// Otherwise, we record the profit at resolution time
|
||||||
|
const profitById: Record<string, number> = {}
|
||||||
|
for (const bet of bets) {
|
||||||
|
if (bet.sale) {
|
||||||
|
const originalBet = betsById[bet.sale.betId]
|
||||||
|
const profit = bet.sale.amount - originalBet.amount
|
||||||
|
profitById[bet.id] = profit
|
||||||
|
profitById[originalBet.id] = profit
|
||||||
|
} else {
|
||||||
|
profitById[bet.id] = resolvedPayout(contract, bet) - bet.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now find the betId with the highest profit
|
||||||
|
const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id
|
||||||
|
const topBettor = useUserById(betsById[topBetId]?.userId)
|
||||||
|
|
||||||
|
// And also the commentId of the comment with the highest profit
|
||||||
|
const topCommentId = sortBy(
|
||||||
|
comments,
|
||||||
|
(c) => c.betId && -profitById[c.betId]
|
||||||
|
)[0]?.id
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-12 max-w-sm">
|
||||||
|
{topCommentId && profitById[topCommentId] > 0 && (
|
||||||
|
<>
|
||||||
|
<Title text="💬 Proven correct" className="!mt-0" />
|
||||||
|
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
||||||
|
<FeedComment
|
||||||
|
contract={contract}
|
||||||
|
comment={commentsById[topCommentId]}
|
||||||
|
tips={tips[topCommentId]}
|
||||||
|
betsBySameUser={[betsById[topCommentId]]}
|
||||||
|
truncate={false}
|
||||||
|
smallAvatar={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-sm text-gray-500">
|
||||||
|
{commentsById[topCommentId].userName} made{' '}
|
||||||
|
{formatMoney(profitById[topCommentId] || 0)}!
|
||||||
|
</div>
|
||||||
|
<Spacer h={16} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* If they're the same, only show the comment; otherwise show both */}
|
||||||
|
{topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && (
|
||||||
|
<>
|
||||||
|
<Title text="💸 Smartest money" className="!mt-0" />
|
||||||
|
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
||||||
|
<FeedBet
|
||||||
|
contract={contract}
|
||||||
|
bet={betsById[topBetId]}
|
||||||
|
hideOutcome={false}
|
||||||
|
smallAvatar={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-sm text-gray-500">
|
||||||
|
{topBettor?.name} made {formatMoney(profitById[topBetId] || 0)}!
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -151,7 +151,7 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
enableGridX={!!width && width >= 800}
|
enableGridX={!!width && width >= 800}
|
||||||
enableArea
|
enableArea
|
||||||
areaBaselineValue={isBinary || isLogScale ? 0 : contract.min}
|
areaBaselineValue={isBinary || isLogScale ? 0 : contract.min}
|
||||||
margin={{ top: 20, right: 20, bottom: 65, left: 40 }}
|
margin={{ top: 20, right: 20, bottom: 25, left: 40 }}
|
||||||
animate={false}
|
animate={false}
|
||||||
sliceTooltip={SliceTooltip}
|
sliceTooltip={SliceTooltip}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const CreateQuestionButton = (props: {
|
||||||
{user ? (
|
{user ? (
|
||||||
<Link href={`/create${query ? query : ''}`} passHref>
|
<Link href={`/create${query ? query : ''}`} passHref>
|
||||||
<button className={clsx(gradient, createButtonStyle)}>
|
<button className={clsx(gradient, createButtonStyle)}>
|
||||||
{overrideText ? overrideText : 'Create a question'}
|
{overrideText ? overrideText : 'Create a market'}
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Comment } from 'common/comment'
|
import { Comment } from 'common/comment'
|
||||||
import { formatPercent } from 'common/util/format'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
|
||||||
import { AnswerBetPanel } from 'web/components/answers/answer-bet-panel'
|
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { Avatar } from 'web/components/avatar'
|
import { Avatar } from 'web/components/avatar'
|
||||||
import { UserLink } from 'web/components/user-page'
|
import { UserLink } from 'web/components/user-page'
|
||||||
import { Linkify } from 'web/components/linkify'
|
import { Linkify } from 'web/components/linkify'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { tradingAllowed } from 'web/lib/firebase/contracts'
|
|
||||||
import { BuyButton } from 'web/components/yes-no-selector'
|
|
||||||
import {
|
import {
|
||||||
CommentInput,
|
CommentInput,
|
||||||
CommentRepliesList,
|
CommentRepliesList,
|
||||||
|
@ -23,7 +18,6 @@ import { useRouter } from 'next/router'
|
||||||
import { groupBy } from 'lodash'
|
import { groupBy } from 'lodash'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { useEvent } from 'web/hooks/use-event'
|
import { useEvent } from 'web/hooks/use-event'
|
||||||
import { getDpmOutcomeProbability } from 'common/calculate-dpm'
|
|
||||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||||
|
|
||||||
export function FeedAnswerCommentGroup(props: {
|
export function FeedAnswerCommentGroup(props: {
|
||||||
|
@ -38,7 +32,6 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
const { username, avatarUrl, name, text } = answer
|
const { username, avatarUrl, name, text } = answer
|
||||||
|
|
||||||
const [replyToUsername, setReplyToUsername] = useState('')
|
const [replyToUsername, setReplyToUsername] = useState('')
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [showReply, setShowReply] = useState(false)
|
const [showReply, setShowReply] = useState(false)
|
||||||
const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null)
|
const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null)
|
||||||
const [highlighted, setHighlighted] = useState(false)
|
const [highlighted, setHighlighted] = useState(false)
|
||||||
|
@ -50,11 +43,6 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
const commentsList = comments.filter(
|
const commentsList = comments.filter(
|
||||||
(comment) => comment.answerOutcome === answer.number.toString()
|
(comment) => comment.answerOutcome === answer.number.toString()
|
||||||
)
|
)
|
||||||
const thisAnswerProb = getDpmOutcomeProbability(
|
|
||||||
contract.totalShares,
|
|
||||||
answer.id
|
|
||||||
)
|
|
||||||
const probPercent = formatPercent(thisAnswerProb)
|
|
||||||
const betsByCurrentUser = (user && betsByUserId[user.id]) ?? []
|
const betsByCurrentUser = (user && betsByUserId[user.id]) ?? []
|
||||||
const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? []
|
const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? []
|
||||||
const isFreeResponseContractPage = !!commentsByCurrentUser
|
const isFreeResponseContractPage = !!commentsByCurrentUser
|
||||||
|
@ -112,27 +100,16 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
}, [answerElementId, router.asPath])
|
}, [answerElementId, router.asPath])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={'relative flex-1 gap-2'} key={answer.id + 'comment'}>
|
<Col className={'relative flex-1 gap-3'} key={answer.id + 'comment'}>
|
||||||
<Modal open={open} setOpen={setOpen}>
|
|
||||||
<AnswerBetPanel
|
|
||||||
answer={answer}
|
|
||||||
contract={contract}
|
|
||||||
closePanel={() => setOpen(false)}
|
|
||||||
className="sm:max-w-84 !rounded-md bg-white !px-8 !py-6"
|
|
||||||
isModal={true}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Row
|
<Row
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'my-4 flex gap-3 space-x-3 transition-all duration-1000',
|
'flex gap-3 space-x-3 pt-4 transition-all duration-1000',
|
||||||
highlighted ? `-m-2 my-3 rounded bg-indigo-500/[0.2] p-2` : ''
|
highlighted ? `-m-2 my-3 rounded bg-indigo-500/[0.2] p-2` : ''
|
||||||
)}
|
)}
|
||||||
id={answerElementId}
|
id={answerElementId}
|
||||||
>
|
>
|
||||||
<div className="px-1">
|
<Avatar username={username} avatarUrl={avatarUrl} />
|
||||||
<Avatar username={username} avatarUrl={avatarUrl} />
|
|
||||||
</div>
|
|
||||||
<Col className="min-w-0 flex-1 lg:gap-1">
|
<Col className="min-w-0 flex-1 lg:gap-1">
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
<UserLink username={username} name={name} /> answered
|
<UserLink username={username} name={name} /> answered
|
||||||
|
@ -144,43 +121,21 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Col className="align-items justify-between gap-4 sm:flex-row">
|
<Col className="align-items justify-between gap-2 sm:flex-row">
|
||||||
<span className="whitespace-pre-line text-lg">
|
<span className="whitespace-pre-line text-lg">
|
||||||
<Linkify text={text} />
|
<Linkify text={text} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Row className="items-center justify-center gap-4">
|
{isFreeResponseContractPage && (
|
||||||
{isFreeResponseContractPage && (
|
<div className={'sm:hidden'}>
|
||||||
<div className={'sm:hidden'}>
|
<button
|
||||||
<button
|
className={'text-xs font-bold text-gray-500 hover:underline'}
|
||||||
className={
|
onClick={() => scrollAndOpenReplyInput(undefined, answer)}
|
||||||
'text-xs font-bold text-gray-500 hover:underline'
|
|
||||||
}
|
|
||||||
onClick={() => scrollAndOpenReplyInput(undefined, answer)}
|
|
||||||
>
|
|
||||||
Reply
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={'align-items flex w-full justify-end gap-4 '}>
|
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
'text-2xl',
|
|
||||||
tradingAllowed(contract) ? 'text-primary' : 'text-gray-500'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{probPercent}
|
Reply
|
||||||
</span>
|
</button>
|
||||||
<BuyButton
|
|
||||||
className={clsx(
|
|
||||||
'btn-sm flex-initial !px-6 sm:flex',
|
|
||||||
tradingAllowed(contract) ? '' : '!hidden'
|
|
||||||
)}
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
{isFreeResponseContractPage && (
|
{isFreeResponseContractPage && (
|
||||||
<div className={'justify-initial hidden sm:block'}>
|
<div className={'justify-initial hidden sm:block'}>
|
||||||
|
@ -207,9 +162,9 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showReply && (
|
{showReply && (
|
||||||
<div className={'ml-6 pt-4'}>
|
<div className={'ml-6'}>
|
||||||
<span
|
<span
|
||||||
className="absolute -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
className="absolute -ml-[1px] mt-[1.25rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<CommentInput
|
<CommentInput
|
||||||
|
|
|
@ -93,6 +93,24 @@ export function BetStatusText(props: {
|
||||||
bet.fills?.some((fill) => fill.matchedBetId === null)) ??
|
bet.fills?.some((fill) => fill.matchedBetId === null)) ??
|
||||||
false
|
false
|
||||||
|
|
||||||
|
const fromProb =
|
||||||
|
hadPoolMatch || isFreeResponse
|
||||||
|
? isPseudoNumeric
|
||||||
|
? formatNumericProbability(bet.probBefore, contract)
|
||||||
|
: formatPercent(bet.probBefore)
|
||||||
|
: isPseudoNumeric
|
||||||
|
? formatNumericProbability(bet.limitProb ?? bet.probBefore, contract)
|
||||||
|
: formatPercent(bet.limitProb ?? bet.probBefore)
|
||||||
|
|
||||||
|
const toProb =
|
||||||
|
hadPoolMatch || isFreeResponse
|
||||||
|
? isPseudoNumeric
|
||||||
|
? formatNumericProbability(bet.probAfter, contract)
|
||||||
|
: formatPercent(bet.probAfter)
|
||||||
|
: isPseudoNumeric
|
||||||
|
? formatNumericProbability(bet.limitProb ?? bet.probAfter, contract)
|
||||||
|
: formatPercent(bet.limitProb ?? bet.probAfter)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{bettor ? (
|
{bettor ? (
|
||||||
|
@ -112,14 +130,9 @@ export function BetStatusText(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
truncate="short"
|
truncate="short"
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{isPseudoNumeric
|
{fromProb === toProb
|
||||||
? ' than ' + formatNumericProbability(bet.probAfter, contract)
|
? `at ${fromProb}`
|
||||||
: ' at ' +
|
: `from ${fromProb} to ${toProb}`}
|
||||||
formatPercent(
|
|
||||||
hadPoolMatch || isFreeResponse
|
|
||||||
? bet.probAfter
|
|
||||||
: bet.limitProb ?? bet.probAfter
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<RelativeTimestamp time={createdTime} />
|
<RelativeTimestamp time={createdTime} />
|
||||||
|
|
|
@ -70,7 +70,7 @@ export function FeedCommentThread(props: {
|
||||||
if (showReply && inputRef) inputRef.focus()
|
if (showReply && inputRef) inputRef.focus()
|
||||||
}, [inputRef, showReply])
|
}, [inputRef, showReply])
|
||||||
return (
|
return (
|
||||||
<div className={'w-full flex-col pr-1'}>
|
<Col className={'w-full gap-3 pr-1'}>
|
||||||
<span
|
<span
|
||||||
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
@ -86,7 +86,7 @@ export function FeedCommentThread(props: {
|
||||||
scrollAndOpenReplyInput={scrollAndOpenReplyInput}
|
scrollAndOpenReplyInput={scrollAndOpenReplyInput}
|
||||||
/>
|
/>
|
||||||
{showReply && (
|
{showReply && (
|
||||||
<div className={'-pb-2 ml-6 flex flex-col pt-5'}>
|
<Col className={'-pb-2 ml-6'}>
|
||||||
<span
|
<span
|
||||||
className="absolute -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
className="absolute -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
@ -106,9 +106,9 @@ export function FeedCommentThread(props: {
|
||||||
setReplyToUsername('')
|
setReplyToUsername('')
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Col>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ export function CommentRepliesList(props: {
|
||||||
id={comment.id}
|
id={comment.id}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative',
|
'relative',
|
||||||
!treatFirstIndexEqually && commentIdx === 0 ? '' : 'mt-3 ml-6'
|
!treatFirstIndexEqually && commentIdx === 0 ? '' : 'ml-6'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/*draw a gray line from the comment to the left:*/}
|
{/*draw a gray line from the comment to the left:*/}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import BetRow from '../bet-row'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
import { ActivityItem } from './activity-items'
|
import { ActivityItem } from './activity-items'
|
||||||
import { useSaveSeenContract } from 'web/hooks/use-seen-contracts'
|
import { useSaveSeenContract } from 'web/hooks/use-seen-contracts'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { trackClick } from 'web/lib/firebase/tracking'
|
import { trackClick } from 'web/lib/firebase/tracking'
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
import NewContractBadge from '../new-contract-badge'
|
import NewContractBadge from '../new-contract-badge'
|
||||||
|
@ -118,6 +119,7 @@ export function FeedQuestion(props: {
|
||||||
const { volumeLabel } = contractMetrics(contract)
|
const { volumeLabel } = contractMetrics(contract)
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
const isNew = createdTime > Date.now() - DAY_MS && !isResolved
|
const isNew = createdTime > Date.now() - DAY_MS && !isResolved
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex gap-2'}>
|
<div className={'flex gap-2'}>
|
||||||
|
@ -149,7 +151,7 @@ export function FeedQuestion(props: {
|
||||||
href={
|
href={
|
||||||
props.contractPath ? props.contractPath : contractPath(contract)
|
props.contractPath ? props.contractPath : contractPath(contract)
|
||||||
}
|
}
|
||||||
onClick={() => trackClick(contract.id)}
|
onClick={() => user && trackClick(user.id, contract.id)}
|
||||||
className="text-lg text-indigo-700 sm:text-xl"
|
className="text-lg text-indigo-700 sm:text-xl"
|
||||||
>
|
>
|
||||||
{question}
|
{question}
|
||||||
|
|
30
web/components/info-box.tsx
Normal file
30
web/components/info-box.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { InformationCircleIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
|
import { Linkify } from './linkify'
|
||||||
|
|
||||||
|
export function InfoBox(props: {
|
||||||
|
title: string
|
||||||
|
text: string
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { title, text, className } = props
|
||||||
|
return (
|
||||||
|
<div className={clsx('rounded-md bg-gray-50 p-4', className)}>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<InformationCircleIcon
|
||||||
|
className="h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<h3 className="text-sm font-medium text-black">{title}</h3>
|
||||||
|
<div className="mt-2 text-sm text-black">
|
||||||
|
<Linkify text={text} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
className={clsx(
|
<Col
|
||||||
className,
|
className={clsx(
|
||||||
'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'
|
className,
|
||||||
)}
|
'min-h-20 group rounded-lg bg-gradient-to-br drop-shadow-sm transition-all',
|
||||||
>
|
getManalinkGradient(info.amount)
|
||||||
<Col className="mx-4 mt-2 -mb-4 text-right text-sm text-gray-100">
|
)}
|
||||||
<div>
|
>
|
||||||
{maxUses != null
|
<Col className="mx-4 mt-2 -mb-4 text-right text-sm text-gray-100">
|
||||||
? `${maxUses - uses}/${maxUses} uses left`
|
<div>
|
||||||
: `Unlimited use`}
|
{maxUses != null
|
||||||
</div>
|
? `${maxUses - uses}/${maxUses} uses left`
|
||||||
<div>
|
: `Unlimited use`}
|
||||||
{expiresTime != null
|
</div>
|
||||||
? `Expires ${fromNow(expiresTime)}`
|
<div>
|
||||||
: 'Never expires'}
|
{expiresTime != null
|
||||||
</div>
|
? `Expires ${fromNow(expiresTime)}`
|
||||||
</Col>
|
: 'Never expires'}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
className="mb-6 block self-center transition-all group-hover:rotate-12"
|
className={clsx(
|
||||||
src="/logo-white.svg"
|
'block h-1/3 w-1/3 self-center transition-all group-hover:rotate-12',
|
||||||
width={200}
|
preview ? 'my-2' : 'w-1/2 md:mb-6 md:h-1/2'
|
||||||
height={200}
|
)}
|
||||||
/>
|
src="/logo-white.svg"
|
||||||
<Row className="justify-end rounded-b-xl bg-white p-4">
|
/>
|
||||||
<Col>
|
<Row className="rounded-b-lg bg-white p-4">
|
||||||
<div className="mb-1 text-xl text-indigo-500">
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'mb-1 text-xl text-indigo-500',
|
||||||
|
getManalinkAmountColor(amount)
|
||||||
|
)}
|
||||||
|
>
|
||||||
{formatMoney(amount)}
|
{formatMoney(amount)}
|
||||||
</div>
|
</div>
|
||||||
<div>{message}</div>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
<div className="text-md mt-2 mb-4 text-gray-500">{message}</div>
|
||||||
<div className="ml-auto">
|
</Col>
|
||||||
<Button onClick={onClaim} disabled={isClaiming}>
|
|
||||||
{user ? 'Claim' : 'Login'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ManalinkCardPreview(props: {
|
export function ManalinkCardFromView(props: {
|
||||||
className?: string
|
className?: string
|
||||||
info: ManalinkInfo
|
link: Manalink
|
||||||
|
highlightedSlug: string
|
||||||
}) {
|
}) {
|
||||||
const { className, info } = props
|
const { className, link, highlightedSlug } = props
|
||||||
const { expiresTime, maxUses, uses, amount, message } = info
|
const { message, amount, expiresTime, maxUses, claims } = link
|
||||||
|
const [showDetails, setShowDetails] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Col>
|
||||||
className={clsx(
|
<Col
|
||||||
className,
|
className={clsx(
|
||||||
' group flex flex-col rounded-lg bg-gradient-to-br from-indigo-200 via-indigo-400 to-indigo-800 shadow-lg transition-all'
|
'group z-10 rounded-lg drop-shadow-sm transition-all hover:drop-shadow-lg',
|
||||||
)}
|
className,
|
||||||
>
|
link.slug === highlightedSlug ? 'shadow-md shadow-indigo-400' : ''
|
||||||
<Col className="mx-4 mt-2 -mb-4 text-right text-xs text-gray-100">
|
)}
|
||||||
<div>
|
>
|
||||||
{maxUses != null
|
<Col
|
||||||
? `${maxUses - uses}/${maxUses} uses left`
|
className={clsx(
|
||||||
: `Unlimited use`}
|
'relative rounded-t-lg bg-gradient-to-br transition-all',
|
||||||
|
getManalinkGradient(link.amount)
|
||||||
|
)}
|
||||||
|
onClick={() => setShowDetails(!showDetails)}
|
||||||
|
>
|
||||||
|
{showDetails && (
|
||||||
|
<ClaimsList
|
||||||
|
className="absolute h-full w-full bg-white opacity-90"
|
||||||
|
link={link}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Col className="mx-4 mt-2 -mb-4 text-right text-xs text-gray-100">
|
||||||
|
<div>
|
||||||
|
{maxUses != null
|
||||||
|
? `${maxUses - claims.length}/${maxUses} uses left`
|
||||||
|
: `Unlimited use`}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{expiresTime != null
|
||||||
|
? `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"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Row className="relative w-full gap-1 rounded-b-lg bg-white px-4 py-2 text-lg">
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'my-auto mb-1 w-full',
|
||||||
|
getManalinkAmountColor(amount)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatMoney(amount)}
|
||||||
|
</div>
|
||||||
|
<ShareIconButton
|
||||||
|
toastClassName={'-left-48 min-w-[250%]'}
|
||||||
|
buttonClassName={'transition-colors'}
|
||||||
|
onCopyButtonClassName={
|
||||||
|
'bg-gray-200 text-gray-600 transition-none hover:bg-gray-200 hover:text-gray-600'
|
||||||
|
}
|
||||||
|
copyPayload={getManalinkUrl(link.slug)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDetails(!showDetails)}
|
||||||
|
className={clsx(
|
||||||
|
contractDetailsButtonClassName,
|
||||||
|
showDetails
|
||||||
|
? 'bg-gray-200 text-gray-600 hover:bg-gray-200 hover:text-gray-600'
|
||||||
|
: ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DotsHorizontalIcon className="h-[24px] w-5" />
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
<div className="mt-2 mb-4 text-xs text-gray-500 md:text-sm">
|
||||||
|
{message || ''}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClaimsList(props: { link: Manalink; className: string }) {
|
||||||
|
const { link, className } = props
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Col className={clsx('px-4 py-2', className)}>
|
||||||
|
<div className="text-md mb-1 mt-2 w-full font-semibold">
|
||||||
|
Claimed by...
|
||||||
</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'
|
||||||
|
@ -164,6 +164,7 @@ function CreateManalinkForm(props: {
|
||||||
<label className="label">Message</label>
|
<label className="label">Message</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder={defaultMessage}
|
placeholder={defaultMessage}
|
||||||
|
maxLength={200}
|
||||||
className="input input-bordered resize-none"
|
className="input input-bordered resize-none"
|
||||||
autoFocus
|
autoFocus
|
||||||
value={newManalink.message}
|
value={newManalink.message}
|
||||||
|
@ -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',
|
||||||
|
|
|
@ -40,6 +40,8 @@ function getNavigation() {
|
||||||
icon: NotificationsIcon,
|
icon: NotificationsIcon,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
||||||
|
|
||||||
...(IS_PRIVATE_MANIFOLD
|
...(IS_PRIVATE_MANIFOLD
|
||||||
? []
|
? []
|
||||||
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
||||||
|
@ -53,7 +55,6 @@ function getMoreNavigation(user?: User | null) {
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return [
|
return [
|
||||||
{ name: 'Leaderboards', href: '/leaderboards' },
|
|
||||||
{ name: 'Charity', href: '/charity' },
|
{ name: 'Charity', href: '/charity' },
|
||||||
{ name: 'Blog', href: 'https://news.manifold.markets' },
|
{ name: 'Blog', href: 'https://news.manifold.markets' },
|
||||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||||
|
@ -62,9 +63,9 @@ function getMoreNavigation(user?: User | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ name: 'Send M$', href: '/links' },
|
{ name: 'Referrals', href: '/referrals' },
|
||||||
{ name: 'Leaderboards', href: '/leaderboards' },
|
|
||||||
{ name: 'Charity', href: '/charity' },
|
{ name: 'Charity', href: '/charity' },
|
||||||
|
{ name: 'Send M$', href: '/links' },
|
||||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||||
{ name: 'About', href: 'https://docs.manifold.markets/$how-to' },
|
{ name: 'About', href: 'https://docs.manifold.markets/$how-to' },
|
||||||
{
|
{
|
||||||
|
@ -78,7 +79,6 @@ function getMoreNavigation(user?: User | null) {
|
||||||
const signedOutNavigation = [
|
const signedOutNavigation = [
|
||||||
{ name: 'Home', href: '/home', icon: HomeIcon },
|
{ name: 'Home', href: '/home', icon: HomeIcon },
|
||||||
{ name: 'Explore', href: '/markets', icon: SearchIcon },
|
{ name: 'Explore', href: '/markets', icon: SearchIcon },
|
||||||
{ name: 'Charity', href: '/charity', icon: HeartIcon },
|
|
||||||
{
|
{
|
||||||
name: 'About',
|
name: 'About',
|
||||||
href: 'https://docs.manifold.markets/$how-to',
|
href: 'https://docs.manifold.markets/$how-to',
|
||||||
|
@ -98,6 +98,7 @@ const signedOutMobileNavigation = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const signedInMobileNavigation = [
|
const signedInMobileNavigation = [
|
||||||
|
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
||||||
...(IS_PRIVATE_MANIFOLD
|
...(IS_PRIVATE_MANIFOLD
|
||||||
? []
|
? []
|
||||||
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
||||||
|
@ -113,11 +114,11 @@ function getMoreMobileNav() {
|
||||||
...(IS_PRIVATE_MANIFOLD
|
...(IS_PRIVATE_MANIFOLD
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{ name: 'Send M$', href: '/links' },
|
{ name: 'Referrals', href: '/referrals' },
|
||||||
{ name: 'Charity', href: '/charity' },
|
{ name: 'Charity', href: '/charity' },
|
||||||
|
{ name: 'Send M$', href: '/links' },
|
||||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||||
]),
|
]),
|
||||||
{ name: 'Leaderboards', href: '/leaderboards' },
|
|
||||||
{
|
{
|
||||||
name: 'Sign out',
|
name: 'Sign out',
|
||||||
href: '#',
|
href: '#',
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ export const useAlgoFeed = (
|
||||||
getDefaultFeed().then((feed) => setAllFeed(feed))
|
getDefaultFeed().then((feed) => setAllFeed(feed))
|
||||||
} else setAllFeed(feed)
|
} else setAllFeed(feed)
|
||||||
|
|
||||||
trackLatency('feed', getTime())
|
trackLatency(user.id, 'feed', getTime())
|
||||||
console.log('"all" feed load time', getTime())
|
console.log('"all" feed load time', getTime())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
27
web/hooks/use-save-referral.ts
Normal file
27
web/hooks/use-save-referral.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import { User, writeReferralInfo } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
|
export const useSaveReferral = (
|
||||||
|
user?: User | null,
|
||||||
|
options?: {
|
||||||
|
defaultReferrer?: string
|
||||||
|
contractId?: string
|
||||||
|
groupId?: string
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { referrer } = router.query as {
|
||||||
|
referrer?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualReferrer = referrer || options?.defaultReferrer
|
||||||
|
|
||||||
|
if (!user && router.isReady && actualReferrer) {
|
||||||
|
writeReferralInfo(actualReferrer, options?.contractId, options?.groupId)
|
||||||
|
}
|
||||||
|
}, [user, router, options])
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { trackView } from 'web/lib/firebase/tracking'
|
import { trackView } from 'web/lib/firebase/tracking'
|
||||||
import { useIsVisible } from './use-is-visible'
|
import { useIsVisible } from './use-is-visible'
|
||||||
|
import { useUser } from './use-user'
|
||||||
|
|
||||||
export const useSeenContracts = () => {
|
export const useSeenContracts = () => {
|
||||||
const [seenContracts, setSeenContracts] = useState<{
|
const [seenContracts, setSeenContracts] = useState<{
|
||||||
|
@ -21,18 +22,19 @@ export const useSaveSeenContract = (
|
||||||
contract: Contract
|
contract: Contract
|
||||||
) => {
|
) => {
|
||||||
const isVisible = useIsVisible(elem)
|
const isVisible = useIsVisible(elem)
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible && user) {
|
||||||
const newSeenContracts = {
|
const newSeenContracts = {
|
||||||
...getSeenContracts(),
|
...getSeenContracts(),
|
||||||
[contract.id]: Date.now(),
|
[contract.id]: Date.now(),
|
||||||
}
|
}
|
||||||
localStorage.setItem(key, JSON.stringify(newSeenContracts))
|
localStorage.setItem(key, JSON.stringify(newSeenContracts))
|
||||||
|
|
||||||
trackView(contract.id)
|
trackView(user.id, contract.id)
|
||||||
}
|
}
|
||||||
}, [isVisible, contract])
|
}, [isVisible, user, contract])
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = 'feed-seen-contracts'
|
const key = 'feed-seen-contracts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useContext, useEffect, useState } from 'react'
|
||||||
import { useFirestoreDocumentData } from '@react-query-firebase/firestore'
|
import { useFirestoreDocumentData } from '@react-query-firebase/firestore'
|
||||||
import { QueryClient } from 'react-query'
|
import { QueryClient } from 'react-query'
|
||||||
|
|
||||||
|
@ -6,32 +6,14 @@ import { doc, DocumentData } from 'firebase/firestore'
|
||||||
import { PrivateUser } from 'common/user'
|
import { PrivateUser } from 'common/user'
|
||||||
import {
|
import {
|
||||||
getUser,
|
getUser,
|
||||||
listenForLogin,
|
|
||||||
listenForPrivateUser,
|
listenForPrivateUser,
|
||||||
listenForUser,
|
|
||||||
User,
|
User,
|
||||||
users,
|
users,
|
||||||
} from 'web/lib/firebase/users'
|
} from 'web/lib/firebase/users'
|
||||||
import { useStateCheckEquality } from './use-state-check-equality'
|
import { AuthContext } from 'web/components/auth-context'
|
||||||
import { identifyUser, setUserProperty } from 'web/lib/service/analytics'
|
|
||||||
|
|
||||||
export const useUser = () => {
|
export const useUser = () => {
|
||||||
const [user, setUser] = useStateCheckEquality<User | null | undefined>(
|
return useContext(AuthContext)
|
||||||
undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => listenForLogin(setUser), [setUser])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
identifyUser(user.id)
|
|
||||||
setUserProperty('username', user.username)
|
|
||||||
|
|
||||||
return listenForUser(user.id, setUser)
|
|
||||||
}
|
|
||||||
}, [user, setUser])
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePrivateUser = (userId?: string) => {
|
export const usePrivateUser = (userId?: string) => {
|
||||||
|
|
|
@ -1,32 +1,28 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { PrivateUser, User } from 'common/user'
|
import { PrivateUser, User } from 'common/user'
|
||||||
import {
|
|
||||||
listenForAllUsers,
|
|
||||||
listenForPrivateUsers,
|
|
||||||
} from 'web/lib/firebase/users'
|
|
||||||
import { groupBy, sortBy, difference } from 'lodash'
|
import { groupBy, sortBy, difference } from 'lodash'
|
||||||
import { getContractsOfUserBets } from 'web/lib/firebase/bets'
|
import { getContractsOfUserBets } from 'web/lib/firebase/bets'
|
||||||
import { useFollows } from './use-follows'
|
import { useFollows } from './use-follows'
|
||||||
import { useUser } from './use-user'
|
import { useUser } from './use-user'
|
||||||
|
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
|
||||||
|
import { DocumentData } from 'firebase/firestore'
|
||||||
|
import { users, privateUsers } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
export const useUsers = () => {
|
export const useUsers = () => {
|
||||||
const [users, setUsers] = useState<User[]>([])
|
const result = useFirestoreQueryData<DocumentData, User[]>(['users'], users, {
|
||||||
|
subscribe: true,
|
||||||
useEffect(() => {
|
includeMetadataChanges: true,
|
||||||
listenForAllUsers(setUsers)
|
})
|
||||||
}, [])
|
return result.data ?? []
|
||||||
|
|
||||||
return users
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePrivateUsers = () => {
|
export const usePrivateUsers = () => {
|
||||||
const [users, setUsers] = useState<PrivateUser[]>([])
|
const result = useFirestoreQueryData<DocumentData, PrivateUser[]>(
|
||||||
|
['private users'],
|
||||||
useEffect(() => {
|
privateUsers,
|
||||||
listenForPrivateUsers(setUsers)
|
{ subscribe: true, includeMetadataChanges: true }
|
||||||
}, [])
|
)
|
||||||
|
return result.data || []
|
||||||
return users
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDiscoverUsers = (userId: string | null | undefined) => {
|
export const useDiscoverUsers = (userId: string | null | undefined) => {
|
||||||
|
|
|
@ -24,7 +24,7 @@ export function groupPath(
|
||||||
groupSlug: string,
|
groupSlug: string,
|
||||||
subpath?:
|
subpath?:
|
||||||
| 'edit'
|
| 'edit'
|
||||||
| 'questions'
|
| 'markets'
|
||||||
| 'about'
|
| 'about'
|
||||||
| typeof GROUP_CHAT_SLUG
|
| typeof GROUP_CHAT_SLUG
|
||||||
| 'leaderboards'
|
| 'leaderboards'
|
||||||
|
|
|
@ -2,16 +2,9 @@ import { doc, collection, setDoc } from 'firebase/firestore'
|
||||||
|
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
import { ClickEvent, LatencyEvent, View } from 'common/tracking'
|
import { ClickEvent, LatencyEvent, View } from 'common/tracking'
|
||||||
import { listenForLogin, User } from './users'
|
|
||||||
|
|
||||||
let user: User | null = null
|
export async function trackView(userId: string, contractId: string) {
|
||||||
if (typeof window !== 'undefined') {
|
const ref = doc(collection(db, 'private-users', userId, 'views'))
|
||||||
listenForLogin((u) => (user = u))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function trackView(contractId: string) {
|
|
||||||
if (!user) return
|
|
||||||
const ref = doc(collection(db, 'private-users', user.id, 'views'))
|
|
||||||
|
|
||||||
const view: View = {
|
const view: View = {
|
||||||
contractId,
|
contractId,
|
||||||
|
@ -21,9 +14,8 @@ export async function trackView(contractId: string) {
|
||||||
return await setDoc(ref, view)
|
return await setDoc(ref, view)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trackClick(contractId: string) {
|
export async function trackClick(userId: string, contractId: string) {
|
||||||
if (!user) return
|
const ref = doc(collection(db, 'private-users', userId, 'events'))
|
||||||
const ref = doc(collection(db, 'private-users', user.id, 'events'))
|
|
||||||
|
|
||||||
const clickEvent: ClickEvent = {
|
const clickEvent: ClickEvent = {
|
||||||
type: 'click',
|
type: 'click',
|
||||||
|
@ -35,11 +27,11 @@ export async function trackClick(contractId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trackLatency(
|
export async function trackLatency(
|
||||||
|
userId: string,
|
||||||
type: 'feed' | 'portfolio',
|
type: 'feed' | 'portfolio',
|
||||||
latency: number
|
latency: number
|
||||||
) {
|
) {
|
||||||
if (!user) return
|
const ref = doc(collection(db, 'private-users', userId, 'latency'))
|
||||||
const ref = doc(collection(db, 'private-users', user.id, 'latency'))
|
|
||||||
|
|
||||||
const latencyEvent: LatencyEvent = {
|
const latencyEvent: LatencyEvent = {
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -15,15 +15,10 @@ import {
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import { getAuth } from 'firebase/auth'
|
import { getAuth } from 'firebase/auth'
|
||||||
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
|
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
|
||||||
import {
|
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'
|
||||||
onIdTokenChanged,
|
|
||||||
GoogleAuthProvider,
|
|
||||||
signInWithPopup,
|
|
||||||
} from 'firebase/auth'
|
|
||||||
import { zip } from 'lodash'
|
import { zip } from 'lodash'
|
||||||
import { app, db } from './init'
|
import { app, db } from './init'
|
||||||
import { PortfolioMetrics, PrivateUser, User } from 'common/user'
|
import { PortfolioMetrics, PrivateUser, User } from 'common/user'
|
||||||
import { createUser } from './api'
|
|
||||||
import {
|
import {
|
||||||
coll,
|
coll,
|
||||||
getValue,
|
getValue,
|
||||||
|
@ -37,13 +32,11 @@ import { safeLocalStorage } from '../util/local'
|
||||||
import { filterDefined } from 'common/util/array'
|
import { filterDefined } from 'common/util/array'
|
||||||
import { addUserToGroupViaId } from 'web/lib/firebase/groups'
|
import { addUserToGroupViaId } from 'web/lib/firebase/groups'
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
import { randomString } from 'common/util/random'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
import { deleteAuthCookies, setAuthCookies } from './auth'
|
|
||||||
|
|
||||||
export const users = coll<User>('users')
|
export const users = coll<User>('users')
|
||||||
export const privateUsers = coll<PrivateUser>('private-users')
|
export const privateUsers = coll<PrivateUser>('private-users')
|
||||||
|
@ -97,7 +90,6 @@ export function listenForPrivateUser(
|
||||||
return listenForValue<PrivateUser>(userRef, setPrivateUser)
|
return listenForValue<PrivateUser>(userRef, setPrivateUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CACHED_USER_KEY = 'CACHED_USER_KEY'
|
|
||||||
const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY'
|
const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY'
|
||||||
const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY'
|
const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY'
|
||||||
const CACHED_REFERRAL_GROUP_ID_KEY = 'CACHED_REFERRAL_GROUP_KEY'
|
const CACHED_REFERRAL_GROUP_ID_KEY = 'CACHED_REFERRAL_GROUP_KEY'
|
||||||
|
@ -130,7 +122,7 @@ export function writeReferralInfo(
|
||||||
local?.setItem(CACHED_REFERRAL_CONTRACT_ID_KEY, contractId)
|
local?.setItem(CACHED_REFERRAL_CONTRACT_ID_KEY, contractId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setCachedReferralInfoForUser(user: User | null) {
|
export async function setCachedReferralInfoForUser(user: User | null) {
|
||||||
if (!user || user.referredByUserId) return
|
if (!user || user.referredByUserId) return
|
||||||
// if the user wasn't created in the last minute, don't bother
|
// if the user wasn't created in the last minute, don't bother
|
||||||
const now = dayjs().utc()
|
const now = dayjs().utc()
|
||||||
|
@ -181,46 +173,6 @@ async function setCachedReferralInfoForUser(user: User | null) {
|
||||||
local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY)
|
local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// used to avoid weird race condition
|
|
||||||
let createUserPromise: Promise<User> | undefined = undefined
|
|
||||||
|
|
||||||
export function listenForLogin(onUser: (user: User | null) => void) {
|
|
||||||
const local = safeLocalStorage()
|
|
||||||
const cachedUser = local?.getItem(CACHED_USER_KEY)
|
|
||||||
onUser(cachedUser && JSON.parse(cachedUser))
|
|
||||||
|
|
||||||
return onIdTokenChanged(auth, async (fbUser) => {
|
|
||||||
if (fbUser) {
|
|
||||||
let user: User | null = await getUser(fbUser.uid)
|
|
||||||
if (!user) {
|
|
||||||
if (createUserPromise == null) {
|
|
||||||
const local = safeLocalStorage()
|
|
||||||
let deviceToken = local?.getItem('device-token')
|
|
||||||
if (!deviceToken) {
|
|
||||||
deviceToken = randomString()
|
|
||||||
local?.setItem('device-token', deviceToken)
|
|
||||||
}
|
|
||||||
createUserPromise = createUser({ deviceToken }).then((r) => r as User)
|
|
||||||
}
|
|
||||||
user = await createUserPromise
|
|
||||||
}
|
|
||||||
onUser(user)
|
|
||||||
|
|
||||||
// Persist to local storage, to reduce login blink next time.
|
|
||||||
// Note: Cap on localStorage size is ~5mb
|
|
||||||
local?.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
|
||||||
setCachedReferralInfoForUser(user)
|
|
||||||
setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken)
|
|
||||||
} else {
|
|
||||||
// User logged out; reset to null
|
|
||||||
onUser(null)
|
|
||||||
createUserPromise = undefined
|
|
||||||
local?.removeItem(CACHED_USER_KEY)
|
|
||||||
deleteAuthCookies()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function firebaseLogin() {
|
export async function firebaseLogin() {
|
||||||
const provider = new GoogleAuthProvider()
|
const provider = new GoogleAuthProvider()
|
||||||
return signInWithPopup(auth, provider)
|
return signInWithPopup(auth, provider)
|
||||||
|
@ -258,16 +210,6 @@ export async function listAllUsers() {
|
||||||
return docs.map((doc) => doc.data())
|
return docs.map((doc) => doc.data())
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listenForAllUsers(setUsers: (users: User[]) => void) {
|
|
||||||
listenForValues(users, setUsers)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listenForPrivateUsers(
|
|
||||||
setUsers: (users: PrivateUser[]) => void
|
|
||||||
) {
|
|
||||||
listenForValues(privateUsers, setUsers)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTopTraders(period: Period) {
|
export function getTopTraders(period: Period) {
|
||||||
const topTraders = query(
|
const topTraders = query(
|
||||||
users,
|
users,
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
"gridjs-react": "5.0.2",
|
"gridjs-react": "5.0.2",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.4",
|
||||||
"next": "12.1.2",
|
"next": "12.2.2",
|
||||||
"node-fetch": "3.2.4",
|
"node-fetch": "3.2.4",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-confetti": "6.0.1",
|
"react-confetti": "6.0.1",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { ArrowLeftIcon } from '@heroicons/react/outline'
|
import { ArrowLeftIcon } from '@heroicons/react/outline'
|
||||||
import { keyBy, sortBy, groupBy, sumBy, mapValues } from 'lodash'
|
|
||||||
|
|
||||||
import { useContractWithPreload } from 'web/hooks/use-contract'
|
import { useContractWithPreload } from 'web/hooks/use-contract'
|
||||||
import { ContractOverview } from 'web/components/contract/contract-overview'
|
import { ContractOverview } from 'web/components/contract/contract-overview'
|
||||||
|
@ -8,9 +7,7 @@ import { BetPanel } from 'web/components/bet-panel'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { ResolutionPanel } from 'web/components/resolution-panel'
|
import { ResolutionPanel } from 'web/components/resolution-panel'
|
||||||
import { Title } from 'web/components/title'
|
|
||||||
import { Spacer } from 'web/components/layout/spacer'
|
import { Spacer } from 'web/components/layout/spacer'
|
||||||
import { listUsers, User, writeReferralInfo } from 'web/lib/firebase/users'
|
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
getContractFromSlug,
|
getContractFromSlug,
|
||||||
|
@ -24,28 +21,26 @@ import { Comment, listAllComments } from 'web/lib/firebase/comments'
|
||||||
import Custom404 from '../404'
|
import Custom404 from '../404'
|
||||||
import { AnswersPanel } from 'web/components/answers/answers-panel'
|
import { AnswersPanel } from 'web/components/answers/answers-panel'
|
||||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||||
import { Leaderboard } from 'web/components/leaderboard'
|
|
||||||
import { resolvedPayout } from 'common/calculate'
|
|
||||||
import { formatMoney } from 'common/util/format'
|
|
||||||
import { useUserById } from 'web/hooks/use-user'
|
|
||||||
import { ContractTabs } from 'web/components/contract/contract-tabs'
|
import { ContractTabs } from 'web/components/contract/contract-tabs'
|
||||||
import { contractTextDetails } from 'web/components/contract/contract-details'
|
import { contractTextDetails } from 'web/components/contract/contract-details'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
import Confetti from 'react-confetti'
|
import Confetti from 'react-confetti'
|
||||||
import { NumericBetPanel } from '../../components/numeric-bet-panel'
|
import { NumericBetPanel } from '../../components/numeric-bet-panel'
|
||||||
import { NumericResolutionPanel } from '../../components/numeric-resolution-panel'
|
import { NumericResolutionPanel } from '../../components/numeric-resolution-panel'
|
||||||
import { FeedComment } from 'web/components/feed/feed-comments'
|
|
||||||
import { FeedBet } from 'web/components/feed/feed-bets'
|
|
||||||
import { useIsIframe } from 'web/hooks/use-is-iframe'
|
import { useIsIframe } from 'web/hooks/use-is-iframe'
|
||||||
import ContractEmbedPage from '../embed/[username]/[contractSlug]'
|
import ContractEmbedPage from '../embed/[username]/[contractSlug]'
|
||||||
import { useBets } from 'web/hooks/use-bets'
|
import { useBets } from 'web/hooks/use-bets'
|
||||||
import { CPMMBinaryContract } from 'common/contract'
|
import { CPMMBinaryContract } from 'common/contract'
|
||||||
import { AlertBox } from 'web/components/alert-box'
|
import { AlertBox } from 'web/components/alert-box'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
import { useTipTxns } from 'web/hooks/use-tip-txns'
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { useLiquidity } from 'web/hooks/use-liquidity'
|
import { useLiquidity } from 'web/hooks/use-liquidity'
|
||||||
import { richTextToString } from 'common/util/parse'
|
import { richTextToString } from 'common/util/parse'
|
||||||
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
|
import {
|
||||||
|
ContractLeaderboard,
|
||||||
|
ContractTopTrades,
|
||||||
|
} from 'web/components/contract/contract-leaderboard'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: {
|
export async function getStaticPropz(props: {
|
||||||
|
@ -157,15 +152,10 @@ export function ContractPageContent(
|
||||||
|
|
||||||
const ogCardProps = getOpenGraphProps(contract)
|
const ogCardProps = getOpenGraphProps(contract)
|
||||||
|
|
||||||
const router = useRouter()
|
useSaveReferral(user, {
|
||||||
|
defaultReferrer: contract.creatorUsername,
|
||||||
useEffect(() => {
|
contractId: contract.id,
|
||||||
const { referrer } = router.query as {
|
})
|
||||||
referrer?: string
|
|
||||||
}
|
|
||||||
if (!user && router.isReady)
|
|
||||||
writeReferralInfo(contract.creatorUsername, contract.id, referrer)
|
|
||||||
}, [user, contract, router])
|
|
||||||
|
|
||||||
const rightSidebar = hasSidePanel ? (
|
const rightSidebar = hasSidePanel ? (
|
||||||
<Col className="gap-4">
|
<Col className="gap-4">
|
||||||
|
@ -267,129 +257,6 @@ export function ContractPageContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContractLeaderboard(props: { contract: Contract; bets: Bet[] }) {
|
|
||||||
const { contract, bets } = props
|
|
||||||
const [users, setUsers] = useState<User[]>()
|
|
||||||
|
|
||||||
const { userProfits, top5Ids } = useMemo(() => {
|
|
||||||
// Create a map of userIds to total profits (including sales)
|
|
||||||
const openBets = bets.filter((bet) => !bet.isSold && !bet.sale)
|
|
||||||
const betsByUser = groupBy(openBets, 'userId')
|
|
||||||
|
|
||||||
const userProfits = mapValues(betsByUser, (bets) =>
|
|
||||||
sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount)
|
|
||||||
)
|
|
||||||
// Find the 5 users with the most profits
|
|
||||||
const top5Ids = Object.entries(userProfits)
|
|
||||||
.sort(([_i1, p1], [_i2, p2]) => p2 - p1)
|
|
||||||
.filter(([, p]) => p > 0)
|
|
||||||
.slice(0, 5)
|
|
||||||
.map(([id]) => id)
|
|
||||||
return { userProfits, top5Ids }
|
|
||||||
}, [contract, bets])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (top5Ids.length > 0) {
|
|
||||||
listUsers(top5Ids).then((users) => {
|
|
||||||
const sortedUsers = sortBy(users, (user) => -userProfits[user.id])
|
|
||||||
setUsers(sortedUsers)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [userProfits, top5Ids])
|
|
||||||
|
|
||||||
return users && users.length > 0 ? (
|
|
||||||
<Leaderboard
|
|
||||||
title="🏅 Top bettors"
|
|
||||||
users={users || []}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: 'Total profit',
|
|
||||||
renderCell: (user) => formatMoney(userProfits[user.id] || 0),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
className="mt-12 max-w-sm"
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContractTopTrades(props: {
|
|
||||||
contract: Contract
|
|
||||||
bets: Bet[]
|
|
||||||
comments: Comment[]
|
|
||||||
tips: CommentTipMap
|
|
||||||
}) {
|
|
||||||
const { contract, bets, comments, tips } = props
|
|
||||||
const commentsById = keyBy(comments, 'id')
|
|
||||||
const betsById = keyBy(bets, 'id')
|
|
||||||
|
|
||||||
// If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit
|
|
||||||
// Otherwise, we record the profit at resolution time
|
|
||||||
const profitById: Record<string, number> = {}
|
|
||||||
for (const bet of bets) {
|
|
||||||
if (bet.sale) {
|
|
||||||
const originalBet = betsById[bet.sale.betId]
|
|
||||||
const profit = bet.sale.amount - originalBet.amount
|
|
||||||
profitById[bet.id] = profit
|
|
||||||
profitById[originalBet.id] = profit
|
|
||||||
} else {
|
|
||||||
profitById[bet.id] = resolvedPayout(contract, bet) - bet.amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now find the betId with the highest profit
|
|
||||||
const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id
|
|
||||||
const topBettor = useUserById(betsById[topBetId]?.userId)
|
|
||||||
|
|
||||||
// And also the commentId of the comment with the highest profit
|
|
||||||
const topCommentId = sortBy(
|
|
||||||
comments,
|
|
||||||
(c) => c.betId && -profitById[c.betId]
|
|
||||||
)[0]?.id
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-12 max-w-sm">
|
|
||||||
{topCommentId && profitById[topCommentId] > 0 && (
|
|
||||||
<>
|
|
||||||
<Title text="💬 Proven correct" className="!mt-0" />
|
|
||||||
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
|
||||||
<FeedComment
|
|
||||||
contract={contract}
|
|
||||||
comment={commentsById[topCommentId]}
|
|
||||||
tips={tips[topCommentId]}
|
|
||||||
betsBySameUser={[betsById[topCommentId]]}
|
|
||||||
truncate={false}
|
|
||||||
smallAvatar={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-sm text-gray-500">
|
|
||||||
{commentsById[topCommentId].userName} made{' '}
|
|
||||||
{formatMoney(profitById[topCommentId] || 0)}!
|
|
||||||
</div>
|
|
||||||
<Spacer h={16} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* If they're the same, only show the comment; otherwise show both */}
|
|
||||||
{topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && (
|
|
||||||
<>
|
|
||||||
<Title text="💸 Smartest money" className="!mt-0" />
|
|
||||||
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
|
||||||
<FeedBet
|
|
||||||
contract={contract}
|
|
||||||
bet={betsById[topBetId]}
|
|
||||||
hideOutcome={false}
|
|
||||||
smallAvatar={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-sm text-gray-500">
|
|
||||||
{topBettor?.name} made {formatMoney(profitById[topBetId] || 0)}!
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOpenGraphProps = (contract: Contract) => {
|
const getOpenGraphProps = (contract: Contract) => {
|
||||||
const {
|
const {
|
||||||
resolution,
|
resolution,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Head from 'next/head'
|
||||||
import Script from 'next/script'
|
import Script from 'next/script'
|
||||||
import { usePreserveScroll } from 'web/hooks/use-preserve-scroll'
|
import { usePreserveScroll } from 'web/hooks/use-preserve-scroll'
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||||
|
import { AuthProvider } from 'web/components/auth-context'
|
||||||
|
|
||||||
function firstLine(msg: string) {
|
function firstLine(msg: string) {
|
||||||
return msg.replace(/\r?\n.*/s, '')
|
return msg.replace(/\r?\n.*/s, '')
|
||||||
|
@ -78,9 +79,11 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<QueryClientProvider client={queryClient}>
|
<AuthProvider>
|
||||||
<Component {...pageProps} />
|
<QueryClientProvider client={queryClient}>
|
||||||
</QueryClientProvider>
|
<Component {...pageProps} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</AuthProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { User } from 'common/user'
|
||||||
import { TextEditor, useTextEditor } from 'web/components/editor'
|
import { TextEditor, useTextEditor } from 'web/components/editor'
|
||||||
import { Checkbox } from 'web/components/checkbox'
|
import { Checkbox } from 'web/components/checkbox'
|
||||||
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
||||||
|
import { Title } from 'web/components/title'
|
||||||
|
|
||||||
export const getServerSideProps = redirectIfLoggedOut('/')
|
export const getServerSideProps = redirectIfLoggedOut('/')
|
||||||
|
|
||||||
|
@ -64,6 +65,8 @@ export default function Create() {
|
||||||
<Page>
|
<Page>
|
||||||
<div className="mx-auto w-full max-w-2xl">
|
<div className="mx-auto w-full max-w-2xl">
|
||||||
<div className="rounded-lg px-6 py-4 sm:py-0">
|
<div className="rounded-lg px-6 py-4 sm:py-0">
|
||||||
|
<Title className="!mt-0" text="Create a market" />
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<div className="form-control w-full">
|
<div className="form-control w-full">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
|
|
|
@ -14,12 +14,7 @@ import {
|
||||||
} from 'web/lib/firebase/groups'
|
} from 'web/lib/firebase/groups'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { UserLink } from 'web/components/user-page'
|
import { UserLink } from 'web/components/user-page'
|
||||||
import {
|
import { firebaseLogin, getUser, User } from 'web/lib/firebase/users'
|
||||||
firebaseLogin,
|
|
||||||
getUser,
|
|
||||||
User,
|
|
||||||
writeReferralInfo,
|
|
||||||
} from 'web/lib/firebase/users'
|
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { listMembers, useGroup, useMembers } from 'web/hooks/use-group'
|
import { listMembers, useGroup, useMembers } from 'web/hooks/use-group'
|
||||||
|
@ -34,7 +29,7 @@ import { Linkify } from 'web/components/linkify'
|
||||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||||
import { Tabs } from 'web/components/layout/tabs'
|
import { Tabs } from 'web/components/layout/tabs'
|
||||||
import { CreateQuestionButton } from 'web/components/create-question-button'
|
import { CreateQuestionButton } from 'web/components/create-question-button'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { GroupChat } from 'web/components/groups/group-chat'
|
import { GroupChat } from 'web/components/groups/group-chat'
|
||||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
|
@ -53,6 +48,7 @@ import { searchInAny } from 'common/util/parse'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
import { CopyLinkButton } from 'web/components/copy-link-button'
|
import { CopyLinkButton } from 'web/components/copy-link-button'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
|
@ -113,7 +109,7 @@ export async function getStaticPaths() {
|
||||||
const groupSubpages = [
|
const groupSubpages = [
|
||||||
undefined,
|
undefined,
|
||||||
GROUP_CHAT_SLUG,
|
GROUP_CHAT_SLUG,
|
||||||
'questions',
|
'markets',
|
||||||
'leaderboards',
|
'leaderboards',
|
||||||
'about',
|
'about',
|
||||||
] as const
|
] as const
|
||||||
|
@ -155,13 +151,11 @@ export default function GroupPage(props: {
|
||||||
const messages = useCommentsOnGroup(group?.id)
|
const messages = useCommentsOnGroup(group?.id)
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
useEffect(() => {
|
|
||||||
const { referrer } = router.query as {
|
useSaveReferral(user, {
|
||||||
referrer?: string
|
defaultReferrer: creator.username,
|
||||||
}
|
groupId: group?.id,
|
||||||
if (!user && router.isReady)
|
})
|
||||||
writeReferralInfo(creator.username, undefined, referrer, group?.id)
|
|
||||||
}, [user, creator, group, router])
|
|
||||||
|
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
const chatDisabled = !group || group.chatDisabled
|
const chatDisabled = !group || group.chatDisabled
|
||||||
|
@ -232,9 +226,9 @@ export default function GroupPage(props: {
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
title: 'Questions',
|
title: 'Markets',
|
||||||
content: questionsTab,
|
content: questionsTab,
|
||||||
href: groupPath(group.slug, 'questions'),
|
href: groupPath(group.slug, 'markets'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Leaderboards',
|
title: 'Leaderboards',
|
||||||
|
@ -253,7 +247,7 @@ export default function GroupPage(props: {
|
||||||
<Page
|
<Page
|
||||||
rightSidebar={showChatSidebar ? chatTab : undefined}
|
rightSidebar={showChatSidebar ? chatTab : undefined}
|
||||||
rightSidebarClassName={showChatSidebar ? '!top-0' : ''}
|
rightSidebarClassName={showChatSidebar ? '!top-0' : ''}
|
||||||
className={showChatSidebar ? '!max-w-none !pb-0' : ''}
|
className={showChatSidebar ? '!max-w-7xl !pb-0' : ''}
|
||||||
>
|
>
|
||||||
<SEO
|
<SEO
|
||||||
title={group.name}
|
title={group.name}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { getContractFromSlug } from 'web/lib/firebase/contracts'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
||||||
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
|
|
||||||
export const getServerSideProps = redirectIfLoggedOut('/')
|
export const getServerSideProps = redirectIfLoggedOut('/')
|
||||||
|
|
||||||
|
@ -21,6 +22,8 @@ const Home = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
useTracking('view home')
|
useTracking('view home')
|
||||||
|
|
||||||
|
useSaveReferral()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Page suspend={!!contract}>
|
<Page suspend={!!contract}>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { LandingPagePanel } from 'web/components/landing-page-panel'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { ManifoldLogo } from 'web/components/nav/manifold-logo'
|
import { ManifoldLogo } from 'web/components/nav/manifold-logo'
|
||||||
import { redirectIfLoggedIn } from 'web/lib/firebase/server-auth'
|
import { redirectIfLoggedIn } from 'web/lib/firebase/server-auth'
|
||||||
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
|
|
||||||
export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => {
|
export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => {
|
||||||
// These hardcoded markets will be shown in the frontpage for signed-out users:
|
// These hardcoded markets will be shown in the frontpage for signed-out users:
|
||||||
|
@ -32,6 +33,9 @@ export default function Home(props: { hotContracts: Contract[] }) {
|
||||||
// on this page and they log in -- in the future we will make some cleaner way
|
// on this page and they log in -- in the future we will make some cleaner way
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
useSaveReferral()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
router.replace('/home')
|
router.replace('/home')
|
||||||
|
|
|
@ -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,12 @@ 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'
|
||||||
|
import { Manalink } from 'common/manalink'
|
||||||
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) {
|
||||||
|
@ -68,12 +68,58 @@ 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} />
|
<ManalinksDisplay
|
||||||
|
unclaimedLinks={unclaimedLinks}
|
||||||
|
highlightedSlug={highlightedSlug}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ManalinksDisplay(props: {
|
||||||
|
unclaimedLinks: Manalink[]
|
||||||
|
highlightedSlug: string
|
||||||
|
}) {
|
||||||
|
const { unclaimedLinks, highlightedSlug } = props
|
||||||
|
const [page, setPage] = useState(0)
|
||||||
|
const start = page * LINKS_PER_PAGE
|
||||||
|
const end = start + LINKS_PER_PAGE
|
||||||
|
const displayedLinks = unclaimedLinks.slice(start, end)
|
||||||
|
|
||||||
|
if (unclaimedLinks.length === 0) {
|
||||||
|
return (
|
||||||
|
<p className="text-gray-500">
|
||||||
|
You don't have any unclaimed manalinks. Send some more to spread the
|
||||||
|
wealth!
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Col className="grid w-full gap-4 md:grid-cols-2">
|
||||||
|
{displayedLinks.map((link) => (
|
||||||
|
<ManalinkCardFromView
|
||||||
|
key={link.slug + link.createdTime}
|
||||||
|
link={link}
|
||||||
|
highlightedSlug={highlightedSlug}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
<Pagination
|
||||||
|
page={page}
|
||||||
|
itemsPerPage={LINKS_PER_PAGE}
|
||||||
|
totalItems={unclaimedLinks.length}
|
||||||
|
setPage={setPage}
|
||||||
|
className="mt-4 bg-transparent"
|
||||||
|
scrollToTop
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 +167,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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
57
web/pages/referrals.tsx
Normal file
57
web/pages/referrals.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { Col } from 'web/components/layout/col'
|
||||||
|
import { SEO } from 'web/components/SEO'
|
||||||
|
import { Title } from 'web/components/title'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { Page } from 'web/components/page'
|
||||||
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
|
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
||||||
|
import { REFERRAL_AMOUNT } from 'common/user'
|
||||||
|
import { CopyLinkButton } from 'web/components/copy-link-button'
|
||||||
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
import { InfoBox } from 'web/components/info-box'
|
||||||
|
|
||||||
|
export const getServerSideProps = redirectIfLoggedOut('/')
|
||||||
|
|
||||||
|
export default function ReferralsPage() {
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
useTracking('view referrals')
|
||||||
|
|
||||||
|
const url = `https://${ENV_CONFIG.domain}?referrer=${user?.username}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<SEO title="Referrals" description="" url="/add-funds" />
|
||||||
|
|
||||||
|
<Col className="items-center">
|
||||||
|
<Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md">
|
||||||
|
<Title className="!mt-0" text="Referrals" />
|
||||||
|
<img
|
||||||
|
className="mb-6 block -scale-x-100 self-center"
|
||||||
|
src="/logo-flapping-with-money.gif"
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
Invite new users to Manifold and get M${REFERRAL_AMOUNT} if they
|
||||||
|
sign up!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CopyLinkButton
|
||||||
|
url={url}
|
||||||
|
tracking="copy referral link"
|
||||||
|
buttonClassName="btn-md rounded-l-none"
|
||||||
|
toastClassName={'-left-28 mt-1'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InfoBox
|
||||||
|
title="FYI"
|
||||||
|
className="mt-4 max-w-md"
|
||||||
|
text="You can also earn the referral bonus from sharing the link to any market or group you've created!"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
BIN
web/public/logo-flapping-with-money.gif
Normal file
BIN
web/public/logo-flapping-with-money.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 287 KiB |
354
web/public/mtg/app.js
Normal file
354
web/public/mtg/app.js
Normal file
|
@ -0,0 +1,354 @@
|
||||||
|
mode = 'PLAY'
|
||||||
|
allData = {}
|
||||||
|
total = 0
|
||||||
|
unseenTotal = 0
|
||||||
|
probList = []
|
||||||
|
nameList = []
|
||||||
|
k = 12
|
||||||
|
extra = 3
|
||||||
|
artDict = {}
|
||||||
|
totalCorrect = 0
|
||||||
|
totalSeen = 0
|
||||||
|
wordsLeft = k + extra
|
||||||
|
imagesLeft = k
|
||||||
|
maxRounds = 20
|
||||||
|
whichGuesser = 'counterspell'
|
||||||
|
un = false
|
||||||
|
online = false
|
||||||
|
firstPrint = false
|
||||||
|
flag = true
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
document.location.search.split('&').forEach((pair) => {
|
||||||
|
let v = pair.split('=')
|
||||||
|
if (v[0] === '?whichguesser') {
|
||||||
|
whichGuesser = v[1]
|
||||||
|
} else if (v[0] === 'un') {
|
||||||
|
un = v[1]
|
||||||
|
} else if (v[0] === 'digital') {
|
||||||
|
online = v[1]
|
||||||
|
} else if (v[0] === 'original') {
|
||||||
|
firstPrint = v[1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let firstFetch = fetch('jsons/' + whichGuesser + page + '.json')
|
||||||
|
fetchToResponse(firstFetch)
|
||||||
|
|
||||||
|
function putIntoMapAndFetch(data) {
|
||||||
|
putIntoMap(data.data)
|
||||||
|
if (data.has_more) {
|
||||||
|
page += 1
|
||||||
|
window.setTimeout(() =>
|
||||||
|
fetchToResponse(fetch('jsons/' + whichGuesser + page + '.json'))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
for (const [key, value] of Object.entries(allData)) {
|
||||||
|
nameList.push(key)
|
||||||
|
probList.push(
|
||||||
|
value.length +
|
||||||
|
(probList.length === 0 ? 0 : probList[probList.length - 1])
|
||||||
|
)
|
||||||
|
unseenTotal = total
|
||||||
|
}
|
||||||
|
window.console.log(allData)
|
||||||
|
window.console.log(total)
|
||||||
|
window.console.log(probList)
|
||||||
|
window.console.log(nameList)
|
||||||
|
if (whichGuesser === 'counterspell') {
|
||||||
|
document.getElementById('guess-type').innerText = 'Counterspell Guesser'
|
||||||
|
} else if (whichGuesser === 'burn') {
|
||||||
|
document.getElementById('guess-type').innerText = 'Match With Hot Singles'
|
||||||
|
}
|
||||||
|
setUpNewGame()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKSamples() {
|
||||||
|
let usedCounters = new Set()
|
||||||
|
let currentTotal = unseenTotal
|
||||||
|
let samples = {}
|
||||||
|
let i = 0
|
||||||
|
while (i < k) {
|
||||||
|
let rand = Math.floor(Math.random() * currentTotal)
|
||||||
|
let count = 0
|
||||||
|
for (const [key, value] of Object.entries(allData)) {
|
||||||
|
if (usedCounters.has(key)) {
|
||||||
|
continue
|
||||||
|
} else if (count >= rand) {
|
||||||
|
usedCounters.add(key)
|
||||||
|
currentTotal -= value.length
|
||||||
|
unseenTotal--
|
||||||
|
let randIndex = Math.floor(Math.random() * value.length)
|
||||||
|
let arts = allData[key].splice(randIndex, 1)
|
||||||
|
samples[arts[0].artImg] = [key, arts[0].normalImg]
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
count += value.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of usedCounters) {
|
||||||
|
if (allData[key].length === 0) {
|
||||||
|
delete allData[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let count = 0
|
||||||
|
while (count < extra) {
|
||||||
|
let rand = Math.floor(Math.random() * total)
|
||||||
|
for (let j = 0; j < nameList.length; j++) {
|
||||||
|
if (j >= rand) {
|
||||||
|
if (usedCounters.has(nameList[j])) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
usedCounters.add(nameList[j])
|
||||||
|
count += 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [samples, usedCounters]
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchToResponse(fetch) {
|
||||||
|
return fetch
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((json) => {
|
||||||
|
putIntoMapAndFetch(json)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function determineIfSkip(card) {
|
||||||
|
if (!un) {
|
||||||
|
if (card.set_type === 'funny') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!online) {
|
||||||
|
if (card.digital) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstPrint) {
|
||||||
|
if (
|
||||||
|
card.reprint === true ||
|
||||||
|
(card.frame_effects && card.frame_effects.includes('showcase'))
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// reskinned card names show in art crop
|
||||||
|
if (card.flavor_name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// don't include racist cards
|
||||||
|
return card.content_warning
|
||||||
|
}
|
||||||
|
|
||||||
|
function putIntoMap(data) {
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
let card = data[i]
|
||||||
|
if (determineIfSkip(card)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let name = card.name
|
||||||
|
// remove slashes from adventure cards
|
||||||
|
if (card.card_faces) {
|
||||||
|
name = card.card_faces[0].name
|
||||||
|
}
|
||||||
|
let normalImg = ''
|
||||||
|
if (card.image_uris.normal) {
|
||||||
|
normalImg = card.image_uris.normal
|
||||||
|
} else if (card.image_uris.large) {
|
||||||
|
normalImg = card.image_uris.large
|
||||||
|
} else if (card.image_uris.small) {
|
||||||
|
normalImg = card.image_uris.small
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let artImg = ''
|
||||||
|
if (card.image_uris.art_crop) {
|
||||||
|
artImg = card.image_uris.art_crop
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total += 1
|
||||||
|
if (!allData[name]) {
|
||||||
|
allData[name] = [{ artImg: artImg, normalImg: normalImg }]
|
||||||
|
} else {
|
||||||
|
allData[name].push({ artImg: artImg, normalImg: normalImg })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shuffleArray(array) {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
let j = Math.floor(Math.random() * (i + 1))
|
||||||
|
let temp = array[i]
|
||||||
|
array[i] = array[j]
|
||||||
|
array[j] = temp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUpNewGame() {
|
||||||
|
wordsLeft = k + extra
|
||||||
|
imagesLeft = k
|
||||||
|
let currentRound = totalSeen / k
|
||||||
|
if (currentRound + 1 === maxRounds) {
|
||||||
|
document.getElementById('round-number').innerText = 'Final Round'
|
||||||
|
} else {
|
||||||
|
document.getElementById('round-number').innerText =
|
||||||
|
'Round ' + (1 + currentRound)
|
||||||
|
}
|
||||||
|
|
||||||
|
setWordsLeft()
|
||||||
|
// select new cards
|
||||||
|
let sampledData = getKSamples()
|
||||||
|
artDict = sampledData[0]
|
||||||
|
let randomImages = Object.keys(artDict)
|
||||||
|
shuffleArray(randomImages)
|
||||||
|
let namesList = Array.from(sampledData[1]).sort()
|
||||||
|
// fill in the new cards and names
|
||||||
|
for (let cardIndex = 1; cardIndex <= k; cardIndex++) {
|
||||||
|
let currCard = document.getElementById('card-' + cardIndex)
|
||||||
|
currCard.classList.remove('incorrect')
|
||||||
|
currCard.dataset.name = ''
|
||||||
|
currCard.dataset.url = randomImages[cardIndex - 1]
|
||||||
|
currCard.style.backgroundImage = "url('" + currCard.dataset.url + "')"
|
||||||
|
}
|
||||||
|
const nameBank = document.querySelector('.names-bank')
|
||||||
|
for (nameIndex = 1; nameIndex <= k + extra; nameIndex++) {
|
||||||
|
currName = document.getElementById('name-' + nameIndex)
|
||||||
|
// window.console.log(currName)
|
||||||
|
currName.innerText = namesList[nameIndex - 1]
|
||||||
|
nameBank.appendChild(currName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAnswers() {
|
||||||
|
let score = k
|
||||||
|
// show the correct full cards
|
||||||
|
for (cardIndex = 1; cardIndex <= k; cardIndex++) {
|
||||||
|
currCard = document.getElementById('card-' + cardIndex)
|
||||||
|
let incorrect = true
|
||||||
|
if (currCard.dataset.name) {
|
||||||
|
let guess = document.getElementById(currCard.dataset.name).innerText
|
||||||
|
// window.console.log(artDict[currCard.dataset.url][0], guess);
|
||||||
|
incorrect = artDict[currCard.dataset.url][0] !== guess
|
||||||
|
// decide if their guess was correct
|
||||||
|
}
|
||||||
|
if (incorrect) currCard.classList.add('incorrect')
|
||||||
|
// tally some kind of score
|
||||||
|
if (incorrect) score--
|
||||||
|
// show the correct card
|
||||||
|
currCard.style.backgroundImage =
|
||||||
|
"url('" + artDict[currCard.dataset.url][1] + "')"
|
||||||
|
}
|
||||||
|
totalSeen += k
|
||||||
|
totalCorrect += score
|
||||||
|
document.getElementById('score-amount').innerText = score + '/' + k
|
||||||
|
document.getElementById('score-percent').innerText = Math.round(
|
||||||
|
(totalCorrect * 100) / totalSeen
|
||||||
|
)
|
||||||
|
document.getElementById('score-amount-total').innerText =
|
||||||
|
totalCorrect + '/' + totalSeen
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMode() {
|
||||||
|
event.preventDefault()
|
||||||
|
if (mode === 'PLAY') {
|
||||||
|
mode = 'ANSWER'
|
||||||
|
document.querySelector('.play-page').classList.add('answer-page')
|
||||||
|
window.console.log(totalSeen)
|
||||||
|
if (totalSeen / k === maxRounds - 1) {
|
||||||
|
document.getElementById('submit').style.display = 'none'
|
||||||
|
} else {
|
||||||
|
document.getElementById('submit').value = 'Next Round'
|
||||||
|
}
|
||||||
|
checkAnswers()
|
||||||
|
} else {
|
||||||
|
mode = 'PLAY'
|
||||||
|
document.querySelector('.play-page').classList.remove('answer-page')
|
||||||
|
document.getElementById('submit').value = 'Submit'
|
||||||
|
setUpNewGame()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowDrop(ev, id) {
|
||||||
|
ev.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag(ev) {
|
||||||
|
ev.dataTransfer.setData('text', ev.target.id)
|
||||||
|
let nameEl = document.querySelector('.selected')
|
||||||
|
if (nameEl) nameEl.classList.remove('selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
function drop(ev, id) {
|
||||||
|
ev.preventDefault()
|
||||||
|
var data = ev.dataTransfer.getData('text')
|
||||||
|
dropOnCard(id, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnDrop(ev) {
|
||||||
|
ev.preventDefault()
|
||||||
|
var data = ev.dataTransfer.getData('text')
|
||||||
|
returnToNameBank(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnToNameBank(name) {
|
||||||
|
document
|
||||||
|
.querySelector('.names-bank')
|
||||||
|
.appendChild(document.getElementById(name))
|
||||||
|
let prevContainer = document.querySelector('[data-name=' + name + ']')
|
||||||
|
if (prevContainer) {
|
||||||
|
prevContainer.dataset.name = ''
|
||||||
|
wordsLeft += 1
|
||||||
|
imagesLeft += 1
|
||||||
|
setWordsLeft()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectName(ev) {
|
||||||
|
if (ev.target.parentNode.classList.contains('names-bank')) {
|
||||||
|
let nameEl = document.querySelector('.selected')
|
||||||
|
if (nameEl) nameEl.classList.remove('selected')
|
||||||
|
ev.target.classList.add('selected')
|
||||||
|
} else {
|
||||||
|
returnToNameBank(ev.target.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropSelected(ev, id) {
|
||||||
|
ev.preventDefault()
|
||||||
|
let nameEl = document.querySelector('.selected')
|
||||||
|
window.console.log('drop selected', nameEl)
|
||||||
|
if (!nameEl) return
|
||||||
|
nameEl.classList.remove('selected')
|
||||||
|
dropOnCard(id, nameEl.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropOnCard(id, data) {
|
||||||
|
let target = document.getElementById('card-' + id)
|
||||||
|
target.appendChild(document.getElementById(data))
|
||||||
|
// if this already has a name, remove that name
|
||||||
|
if (target.dataset.name) {
|
||||||
|
returnToNameBank(target.dataset.name)
|
||||||
|
}
|
||||||
|
// remove name data from a previous card if there is one
|
||||||
|
let prevContainer = document.querySelector('[data-name=' + data + ']')
|
||||||
|
if (prevContainer) {
|
||||||
|
prevContainer.dataset.name = ''
|
||||||
|
} else {
|
||||||
|
wordsLeft -= 1
|
||||||
|
imagesLeft -= 1
|
||||||
|
setWordsLeft()
|
||||||
|
}
|
||||||
|
target.dataset.name = data
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWordsLeft() {
|
||||||
|
document.getElementById('words-left').innerText =
|
||||||
|
'Unused Card Names: ' + wordsLeft + '/Images: ' + imagesLeft
|
||||||
|
}
|
559
web/public/mtg/guess.html
Normal file
559
web/public/mtg/guess.html
Normal file
|
@ -0,0 +1,559 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- Google Tag Manager -->
|
||||||
|
<script>
|
||||||
|
;(function (w, d, s, l, i) {
|
||||||
|
w[l] = w[l] || []
|
||||||
|
w[l].push({
|
||||||
|
'gtm.start': new Date().getTime(),
|
||||||
|
event: 'gtm.js',
|
||||||
|
})
|
||||||
|
var f = d.getElementsByTagName(s)[0],
|
||||||
|
j = d.createElement(s),
|
||||||
|
dl = l !== 'dataLayer' ? '&l=' + l : ''
|
||||||
|
j.async = true
|
||||||
|
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl
|
||||||
|
f.parentNode.insertBefore(j, f)
|
||||||
|
})(window, document, 'script', 'dataLayer', 'GTM-M3MBVGG')
|
||||||
|
</script>
|
||||||
|
<!-- End Google Tag Manager -->
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script type="text/javascript" src="app.js"></script>
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
font-family: Georgia, 'Times New Roman', Times, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 230px;
|
||||||
|
height: 208px;
|
||||||
|
border: 5px solid lightgrey;
|
||||||
|
margin: 5px;
|
||||||
|
align-items: flex-end;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 11px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
/*background-size: contain;*/
|
||||||
|
background-size: 220px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
transition: height 1s, background-image 1s, border 0.4s 0.6s;
|
||||||
|
background-position-y: calc(50% - 18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:not([data-name^='name'])::after {
|
||||||
|
content: '';
|
||||||
|
height: 34px;
|
||||||
|
background: white;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-page .card {
|
||||||
|
height: 350px;
|
||||||
|
/*padding-top: 310px;*/
|
||||||
|
/*background-size: cover;*/
|
||||||
|
overflow: hidden;
|
||||||
|
border-color: rgb(0, 146, 156);
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-page .card.incorrect {
|
||||||
|
border-color: rgb(216, 27, 96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.names-bank {
|
||||||
|
position: fixed;
|
||||||
|
padding: 10px 10px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.names-bank .name {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-page .names-bank .name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-page .names-bank .word-count {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-count {
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgb(255, 193, 7);
|
||||||
|
width: 200px;
|
||||||
|
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.names-bank .score {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-page .names-bank .score {
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 1.2s 0.2s;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
width: 230px;
|
||||||
|
min-height: 36px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: lightgrey;
|
||||||
|
padding: 8px 12px 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .name {
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
background-color: cadetblue;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit:hover {
|
||||||
|
background-color: rgb(0, 146, 156);
|
||||||
|
}
|
||||||
|
|
||||||
|
#newGame {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background-color: lightpink;
|
||||||
|
border: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.7em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#newGame:hover {
|
||||||
|
background-color: coral;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (orientation: landscape) and (max-height: 680px) {
|
||||||
|
/* CSS applied when the device is in landscape mode*/
|
||||||
|
.names-bank {
|
||||||
|
padding: 0;
|
||||||
|
top: 0;
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-count {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-right: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (orientation: portrait) and (max-width: 1100px) {
|
||||||
|
body {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-page {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.names-bank {
|
||||||
|
flex-direction: row;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
/* position: fixed; */
|
||||||
|
padding: 10px 10px 40px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-page .names-bank {
|
||||||
|
min-width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.names-bank .name {
|
||||||
|
margin: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.names-bank .score {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-page .names-bank .score {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-count {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 300px;
|
||||||
|
background-size: 300px;
|
||||||
|
height: 266px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-page .card {
|
||||||
|
height: 454px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Google Tag Manager (noscript) -->
|
||||||
|
<noscript>
|
||||||
|
<iframe
|
||||||
|
src="https://www.googletagmanager.com/ns.html?id=GTM-M3MBVGG"
|
||||||
|
height="0"
|
||||||
|
width="0"
|
||||||
|
style="display: none; visibility: hidden"
|
||||||
|
></iframe>
|
||||||
|
</noscript>
|
||||||
|
<!-- End Google Tag Manager (noscript) -->
|
||||||
|
|
||||||
|
<h1><span id="guess-type"></span>: <span id="round-number"></span></h1>
|
||||||
|
|
||||||
|
<div class="play-page">
|
||||||
|
<div
|
||||||
|
class="names-bank"
|
||||||
|
ondrop="returnDrop(event)"
|
||||||
|
ondragover="event.preventDefault()"
|
||||||
|
>
|
||||||
|
<div class="score">
|
||||||
|
YOUR SCORE
|
||||||
|
<div>Correct Answers This Round: <span id="score-amount"></span></div>
|
||||||
|
<div>
|
||||||
|
Correct Answers In Total: <span id="score-amount-total"></span>
|
||||||
|
</div>
|
||||||
|
<div>Overall Percent: <span id="score-percent"></span>%</div>
|
||||||
|
</div>
|
||||||
|
<div class="word-count"><span id="words-left"></span></div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-1"
|
||||||
|
>
|
||||||
|
Name 1
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-2"
|
||||||
|
>
|
||||||
|
Name 2
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-3"
|
||||||
|
>
|
||||||
|
Name 3
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-4"
|
||||||
|
>
|
||||||
|
Name 4
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-5"
|
||||||
|
>
|
||||||
|
Name 5
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-6"
|
||||||
|
>
|
||||||
|
Name 6
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-7"
|
||||||
|
>
|
||||||
|
Name 7
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-8"
|
||||||
|
>
|
||||||
|
Name 8
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-9"
|
||||||
|
>
|
||||||
|
Name 9
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-10"
|
||||||
|
>
|
||||||
|
Name 10
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-11"
|
||||||
|
>
|
||||||
|
Name 11
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-12"
|
||||||
|
>
|
||||||
|
Name 12
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-13"
|
||||||
|
>
|
||||||
|
Name 13
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-14"
|
||||||
|
>
|
||||||
|
Name 14
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
onClick="selectName(event)"
|
||||||
|
id="name-15"
|
||||||
|
>
|
||||||
|
Name 15
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form onsubmit="toggleMode(event)">
|
||||||
|
<div class="cards-container">
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event,1)"
|
||||||
|
ondragover="allowDrop(event,1)"
|
||||||
|
onclick="dropSelected(event, 1)"
|
||||||
|
id="card-1"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event,2)"
|
||||||
|
ondragover="allowDrop(event,2)"
|
||||||
|
onclick="dropSelected(event, 2)"
|
||||||
|
id="card-2"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event,3)"
|
||||||
|
ondragover="allowDrop(event,3)"
|
||||||
|
onclick="dropSelected(event, 3)"
|
||||||
|
id="card-3"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event,4)"
|
||||||
|
ondragover="allowDrop(event,4)"
|
||||||
|
onclick="dropSelected(event, 4)"
|
||||||
|
id="card-4"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event,5)"
|
||||||
|
ondragover="allowDrop(event,5)"
|
||||||
|
onclick="dropSelected(event, 5)"
|
||||||
|
id="card-5"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event, 6)"
|
||||||
|
ondragover="allowDrop(event,6)"
|
||||||
|
onclick="dropSelected(event,6)"
|
||||||
|
id="card-6"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event,7)"
|
||||||
|
ondragover="allowDrop(event,7)"
|
||||||
|
onclick="dropSelected(event, 7)"
|
||||||
|
id="card-7"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event,8)"
|
||||||
|
ondragover="allowDrop(event,8)"
|
||||||
|
onclick="dropSelected(event, 8)"
|
||||||
|
id="card-8"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event,9)"
|
||||||
|
ondragover="allowDrop(event,9)"
|
||||||
|
onclick="dropSelected(event, 9)"
|
||||||
|
id="card-9"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event,10)"
|
||||||
|
ondragover="allowDrop(event,10)"
|
||||||
|
onclick="dropSelected(event, 10)"
|
||||||
|
id="card-10"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event,11)"
|
||||||
|
ondragover="allowDrop(event,11)"
|
||||||
|
onclick="dropSelected(event, 11)"
|
||||||
|
id="card-11"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
ondrop="drop(event,12)"
|
||||||
|
ondragover="allowDrop(event,12)"
|
||||||
|
onclick="dropSelected(event, 12)"
|
||||||
|
id="card-12"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<input type="submit" id="submit" value="Submit" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="position: absolute; top: 0; left: 0; right: 0; color: grey">
|
||||||
|
<form method="get" action="index.html">
|
||||||
|
<input type="submit" id="newGame" value="New Game" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div style="margin: -40px 0 0; height: 60px">
|
||||||
|
<a href="https://paypal.me/idamayer">Donate, buy us a boba 🧋</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-size: 0.9em;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
color: grey;
|
||||||
|
font-style: italic;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
made by
|
||||||
|
<a
|
||||||
|
style="color: rgb(0, 146, 156); font-style: italic"
|
||||||
|
href="https://idamayer.com"
|
||||||
|
>Ida Mayer</a
|
||||||
|
>
|
||||||
|
&
|
||||||
|
<a
|
||||||
|
style="color: rgb(0, 146, 156); font-style: italic"
|
||||||
|
href="mailto:alexlien.alien@gmail.com"
|
||||||
|
>Alex Lien</a
|
||||||
|
>, 2022
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
92
web/public/mtg/importCards.py
Normal file
92
web/public/mtg/importCards.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
# add category name here
|
||||||
|
allCategories = ['counterspell', 'beast', 'terror', 'wrath', 'burn']
|
||||||
|
|
||||||
|
|
||||||
|
def generate_initial_query(category):
|
||||||
|
string_query = 'https://api.scryfall.com/cards/search?q='
|
||||||
|
if category == 'counterspell':
|
||||||
|
string_query += 'otag%3Acounterspell+t%3Ainstant+not%3Aadventure'
|
||||||
|
elif category == 'beast':
|
||||||
|
string_query += '-type%3Alegendary+type%3Abeast+-type%3Atoken'
|
||||||
|
elif category == 'terror':
|
||||||
|
string_query += 'otag%3Acreature-removal+o%3A%2Fdestroy+target.%2A+%28creature%7Cpermanent%29%2F+%28t' \
|
||||||
|
'%3Ainstant+or+t%3Asorcery%29+o%3Atarget+not%3Aadventure'
|
||||||
|
elif category == 'wrath':
|
||||||
|
string_query += 'otag%3Asweeper-creature+%28t%3Ainstant+or+t%3Asorcery%29+not%3Aadventure'
|
||||||
|
elif category == 'burn':
|
||||||
|
string_query += '%28c>%3Dr+or+mana>%3Dr%29+%28o%3A%2Fdamage+to+them%2F+or+%28o%3Adeals+o%3Adamage+o%3A' \
|
||||||
|
'%2Fcontroller%28%5C.%7C+%29%2F%29+or+o%3A%2F~+deals+%28.%7C..%29+damage+to+%28any+target%7C' \
|
||||||
|
'.*player%28%5C.%7C+or+planeswalker%29%7C.*opponent%28%5C.%7C+or+planeswalker%29%29%2F%29' \
|
||||||
|
'+%28type%3Ainstant+or+type%3Asorcery%29+not%3Aadventure'
|
||||||
|
# add category string query here
|
||||||
|
string_query += '+-%28set%3Asld+%28%28cn>%3D231+cn<%3D233%29+or+%28cn>%3D321+cn<%3D324%29+or+%28cn>%3D185+cn' \
|
||||||
|
'<%3D189%29+or+%28cn>%3D138+cn<%3D142%29+or+%28cn>%3D364+cn<%3D368%29+or+cn%3A669+or+cn%3A670%29' \
|
||||||
|
'%29+-name%3A%2F%5EA-%2F+not%3Adfc+not%3Asplit+-set%3Acmb2+-set%3Acmb1+-set%3Aplist+-set%3Adbl' \
|
||||||
|
'+-frame%3Aextendedart+language%3Aenglish&unique=art&page='
|
||||||
|
print(string_query)
|
||||||
|
return string_query
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_and_write_all(category, query):
|
||||||
|
count = 1
|
||||||
|
will_repeat = True
|
||||||
|
while will_repeat:
|
||||||
|
will_repeat = fetch_and_write(category, query, count)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_and_write(category, query, count):
|
||||||
|
query += str(count)
|
||||||
|
response = requests.get(f"{query}").json()
|
||||||
|
time.sleep(0.1)
|
||||||
|
with open('jsons/' + category + str(count) + '.json', 'w') as f:
|
||||||
|
json.dump(to_compact_write_form(response), f)
|
||||||
|
return response['has_more']
|
||||||
|
|
||||||
|
|
||||||
|
def to_compact_write_form(response):
|
||||||
|
fieldsToUse = ['has_more']
|
||||||
|
fieldsInCard = ['name', 'image_uris', 'content_warning', 'flavor_name', 'reprint', 'frame_effects', 'digital',
|
||||||
|
'set_type']
|
||||||
|
smallJson = dict()
|
||||||
|
data = []
|
||||||
|
# write all fields needed in response
|
||||||
|
for field in fieldsToUse:
|
||||||
|
smallJson[field] = response[field]
|
||||||
|
# write all fields needed in card
|
||||||
|
for card in response['data']:
|
||||||
|
write_card = dict()
|
||||||
|
for field in fieldsInCard:
|
||||||
|
if field == 'name' and 'card_faces' in card:
|
||||||
|
write_card['name'] = card['card_faces'][0]['name']
|
||||||
|
elif field == 'image_uris':
|
||||||
|
write_card['image_uris'] = write_image_uris(card['image_uris'])
|
||||||
|
elif field in card:
|
||||||
|
write_card[field] = card[field]
|
||||||
|
data.append(write_card)
|
||||||
|
smallJson['data'] = data
|
||||||
|
return smallJson
|
||||||
|
|
||||||
|
|
||||||
|
# only write images needed
|
||||||
|
def write_image_uris(card_image_uris):
|
||||||
|
image_uris = dict()
|
||||||
|
if 'normal' in card_image_uris:
|
||||||
|
image_uris['normal'] = card_image_uris['normal']
|
||||||
|
elif 'large' in card_image_uris:
|
||||||
|
image_uris['normal'] = card_image_uris['large']
|
||||||
|
elif 'small' in card_image_uris:
|
||||||
|
image_uris['normal'] = card_image_uris['small']
|
||||||
|
if card_image_uris:
|
||||||
|
image_uris['art_crop'] = card_image_uris['art_crop']
|
||||||
|
return image_uris
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for category in allCategories:
|
||||||
|
print(category)
|
||||||
|
fetch_and_write_all(category, generate_initial_query(category))
|
202
web/public/mtg/index.html
Normal file
202
web/public/mtg/index.html
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- Google Tag Manager -->
|
||||||
|
<script>
|
||||||
|
;(function (w, d, s, l, i) {
|
||||||
|
w[l] = w[l] || []
|
||||||
|
w[l].push({
|
||||||
|
'gtm.start': new Date().getTime(),
|
||||||
|
event: 'gtm.js',
|
||||||
|
})
|
||||||
|
var f = d.getElementsByTagName(s)[0],
|
||||||
|
j = d.createElement(s),
|
||||||
|
dl = l !== 'dataLayer' ? '&l=' + l : ''
|
||||||
|
j.async = true
|
||||||
|
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl
|
||||||
|
f.parentNode.insertBefore(j, f)
|
||||||
|
})(window, document, 'script', 'dataLayer', 'GTM-M3MBVGG')
|
||||||
|
</script>
|
||||||
|
<!-- End Google Tag Manager -->
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
font-family: Georgia, 'Times New Roman', Times, serif;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h3 {
|
||||||
|
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
background-color: cadetblue;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit:hover {
|
||||||
|
background-color: rgb(0, 146, 156);
|
||||||
|
}
|
||||||
|
|
||||||
|
[type='radio'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type='radio'] + label.radio-label {
|
||||||
|
background: lightgrey;
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.radio-label:hover {
|
||||||
|
background: darkgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type='radio']:checked + label.radio-label {
|
||||||
|
background: lightcoral;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label h3 {
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 67px;
|
||||||
|
height: 48px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 70px 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addl-options {
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
right: 30px;
|
||||||
|
background-color: white;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addl-options > summary {
|
||||||
|
list-style: none;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Google Tag Manager (noscript) -->
|
||||||
|
<noscript>
|
||||||
|
<iframe
|
||||||
|
src="https://www.googletagmanager.com/ns.html?id=GTM-M3MBVGG"
|
||||||
|
height="0"
|
||||||
|
width="0"
|
||||||
|
style="display: none; visibility: hidden"
|
||||||
|
></iframe>
|
||||||
|
</noscript>
|
||||||
|
<!-- End Google Tag Manager (noscript) -->
|
||||||
|
<h1>Magic the Guessering</h1>
|
||||||
|
<div class="play-page" style="justify-content: center">
|
||||||
|
<form
|
||||||
|
method="get"
|
||||||
|
action="guess.html"
|
||||||
|
style="display: flex; flex-direction: column; align-items: center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="counterspell"
|
||||||
|
name="whichguesser"
|
||||||
|
value="counterspell"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="radio-label" for="counterspell">
|
||||||
|
<img
|
||||||
|
class="thumbnail"
|
||||||
|
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/7/1/71cfcba5-1571-48b8-a3db-55dca135506e.jpg?1562843855"
|
||||||
|
/>
|
||||||
|
<h3>Counterspell Guesser</h3></label
|
||||||
|
><br />
|
||||||
|
|
||||||
|
<input type="radio" id="burn" name="whichguesser" value="burn" />
|
||||||
|
<label class="radio-label" for="burn">
|
||||||
|
<img
|
||||||
|
class="thumbnail"
|
||||||
|
src="https://c1.scryfall.com/file/scryfall-cards/art_crop/front/6/0/60b2fae1-242b-45e0-a757-b1adc02c06f3.jpg?1562760596"
|
||||||
|
/>
|
||||||
|
<h3>Match With Hot Singles</h3></label
|
||||||
|
><br />
|
||||||
|
|
||||||
|
<details id="addl-options">
|
||||||
|
<summary>
|
||||||
|
<img
|
||||||
|
src="https://mythicspoiler.com/images/buttons/ustset.png"
|
||||||
|
style="width: 32px; vertical-align: top"
|
||||||
|
/>
|
||||||
|
Options
|
||||||
|
</summary>
|
||||||
|
<input type="checkbox" name="digital" id="digital" checked />
|
||||||
|
<label for="digital">include digital cards</label>
|
||||||
|
<br />
|
||||||
|
<input type="checkbox" name="un" id="un" checked />
|
||||||
|
<label for="un">include un-cards</label>
|
||||||
|
<br />
|
||||||
|
<input type="checkbox" name="original" id="original" />
|
||||||
|
<label for="original">restrict to only original printing</label>
|
||||||
|
</details>
|
||||||
|
<input type="submit" id="submit" value="Play" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: -40px 0 0; height: 60px">
|
||||||
|
<a href="https://paypal.me/idamayer">Donate, buy us a boba 🧋</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-size: 0.9em;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
color: grey;
|
||||||
|
font-style: italic;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
made by
|
||||||
|
<a
|
||||||
|
style="color: rgb(0, 146, 156); font-style: italic"
|
||||||
|
href="https://idamayer.com"
|
||||||
|
>Ida Mayer</a
|
||||||
|
>
|
||||||
|
&
|
||||||
|
<a
|
||||||
|
style="color: rgb(0, 146, 156); font-style: italic"
|
||||||
|
href="mailto:alexlien.alien@gmail.com"
|
||||||
|
>Alex Lien</a
|
||||||
|
>, 2022
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
web/public/mtg/jsons/burn1.json
Normal file
1
web/public/mtg/jsons/burn1.json
Normal file
File diff suppressed because one or more lines are too long
1
web/public/mtg/jsons/burn2.json
Normal file
1
web/public/mtg/jsons/burn2.json
Normal file
File diff suppressed because one or more lines are too long
1
web/public/mtg/jsons/burn3.json
Normal file
1
web/public/mtg/jsons/burn3.json
Normal file
File diff suppressed because one or more lines are too long
1
web/public/mtg/jsons/counterspell1.json
Normal file
1
web/public/mtg/jsons/counterspell1.json
Normal file
File diff suppressed because one or more lines are too long
1
web/public/mtg/jsons/counterspell2.json
Normal file
1
web/public/mtg/jsons/counterspell2.json
Normal file
File diff suppressed because one or more lines are too long
1
web/public/mtg/jsons/counterspell3.json
Normal file
1
web/public/mtg/jsons/counterspell3.json
Normal file
File diff suppressed because one or more lines are too long
178
yarn.lock
178
yarn.lock
|
@ -2385,10 +2385,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b"
|
resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b"
|
||||||
integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==
|
integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==
|
||||||
|
|
||||||
"@next/env@12.1.2":
|
"@next/env@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.2.tgz#4b0f5fd448ac60b821d2486d2987948e3a099f03"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.2.tgz#cc1a0a445bd254499e30f632968c03192455f4cc"
|
||||||
integrity sha512-A/P4ysmFScBFyu1ZV0Mr1Y89snyQhqGwsCrkEpK+itMF+y+pMqBoPVIyakUf4LXqGWJGiGFuIerihvSG70Ad8Q==
|
integrity sha512-BqDwE4gDl1F608TpnNxZqrCn6g48MBjvmWFEmeX5wEXDXh3IkAOw6ASKUgjT8H4OUePYFqghDFUss5ZhnbOUjw==
|
||||||
|
|
||||||
"@next/eslint-plugin-next@12.1.6":
|
"@next/eslint-plugin-next@12.1.6":
|
||||||
version "12.1.6"
|
version "12.1.6"
|
||||||
|
@ -2397,65 +2397,70 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "7.1.7"
|
glob "7.1.7"
|
||||||
|
|
||||||
"@next/swc-android-arm-eabi@12.1.2":
|
"@next/swc-android-arm-eabi@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.2.tgz#675e952d9032ac7bec02f3f413c17d33bbd90857"
|
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.2.tgz#f6c4111e6371f73af6bf80c9accb3d96850a92cd"
|
||||||
integrity sha512-iwalfLBhYmCIlj09czFbovj1SmTycf0AGR8CB357wgmEN8xIuznIwSsCH87AhwQ9apfNtdeDhxvuKmhS9T3FqQ==
|
integrity sha512-VHjuCHeq9qCprUZbsRxxM/VqSW8MmsUtqB5nEpGEgUNnQi/BTm/2aK8tl7R4D0twGKRh6g1AAeFuWtXzk9Z/vQ==
|
||||||
|
|
||||||
"@next/swc-android-arm64@12.1.2":
|
"@next/swc-android-arm64@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.2.tgz#d9710c50853235f258726b19a649df9c29a49682"
|
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.2.tgz#b69de59c51e631a7600439e7a8993d6e82f3369e"
|
||||||
integrity sha512-ZoR0Vx7czJhTgRAcFbzTKQc2n2ChC036/uc6PbgYiI/LreEnfmsV/CiREP0pUVs5ndntOX8kBA3BSbh4zCO5tQ==
|
integrity sha512-v5EYzXUOSv0r9mO/2PX6mOcF53k8ndlu9yeFHVAWW1Dhw2jaJcvTRcCAwYYN8Q3tDg0nH3NbEltJDLKmcJOuVA==
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@12.1.2":
|
"@next/swc-darwin-arm64@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.2.tgz#aadd21b711c82b3efa9b4ecf7665841259e1fa7e"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.2.tgz#80157c91668eff95b72d052428c353eab0fc4c50"
|
||||||
integrity sha512-VXv7lpqFjHwkK65CZHkjvBxlSBTG+l3O0Zl2zHniHj0xHzxJZvR8VFjV2zIMZCYSfVqeQ5yt2rjwuQ9zbpGtXQ==
|
integrity sha512-JCoGySHKGt+YBk7xRTFGx1QjrnCcwYxIo3yGepcOq64MoiocTM3yllQWeOAJU2/k9MH0+B5E9WUSme4rOCBbpA==
|
||||||
|
|
||||||
"@next/swc-darwin-x64@12.1.2":
|
"@next/swc-darwin-x64@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.2.tgz#3b1a389828f5c88ecb828a6394692fdeaf175081"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.2.tgz#12be2f58e676fccff3d48a62921b9927ed295133"
|
||||||
integrity sha512-evXxJQnXEnU+heWyun7d0UV6bhBcmoiyFGR3O3v9qdhGbeXh+SXYVxRO69juuh6V7RWRdlb1KQ0rGUNa1k0XSw==
|
integrity sha512-dztDtvfkhUqiqpXvrWVccfGhLe44yQ5tQ7B4tBfnsOR6vxzI9DNPHTlEOgRN9qDqTAcFyPxvg86mn4l8bB9Jcw==
|
||||||
|
|
||||||
"@next/swc-linux-arm-gnueabihf@12.1.2":
|
"@next/swc-freebsd-x64@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.2.tgz#db4371ca716bf94c94d4f6b001ac3c9d08d97d79"
|
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.2.tgz#de1363431a49059f1efb8c0f86ce6a79c53b3a95"
|
||||||
integrity sha512-LJV/wo6R0Ot7Y/20bZs00aBG4J333RT6H/5Q2AROE4Hnx7cenSktSnfU6WCnJgzYLSIHdbLs549LcZMULuVquw==
|
integrity sha512-JUnXB+2xfxqsAvhFLPJpU1NeyDsvJrKoOjpV7g3Dxbno2Riu4tDKn3kKF886yleAuD/1qNTUCpqubTvbbT2VoA==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu@12.1.2":
|
"@next/swc-linux-arm-gnueabihf@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.2.tgz#0e71db03b8b12ed315c8be7d15392ecefe562b7c"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.2.tgz#d5b8e0d1bb55bbd9db4d2fec018217471dc8b9e6"
|
||||||
integrity sha512-fjlYU1Y8kVjjRKyuyQBYLHPxjGOS2ox7U8TqAvtgKvd2PxqdsgW4sP+VDovRVPrZlGXNllKoJiqMO1OoR9fB6w==
|
integrity sha512-XeYC/qqPLz58R4pjkb+x8sUUxuGLnx9QruC7/IGkK68yW4G17PHwKI/1njFYVfXTXUukpWjcfBuauWwxp9ke7Q==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl@12.1.2":
|
"@next/swc-linux-arm64-gnu@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.2.tgz#f1b055793da1c12167ed3b6e32aef8289721a1fb"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.2.tgz#3bc75984e1d5ec8f59eb53702cc382d8e1be2061"
|
||||||
integrity sha512-Y1JRDMHqSjLObjyrD1hf6ePrJcOF/mkw+LbAzoNgrHL1dSuIAqcz3jYunJt8T7Yw48xSJy6LPSL9BclAHwEwOA==
|
integrity sha512-d6jT8xgfKYFkzR7J0OHo2D+kFvY/6W8qEo6/hmdrTt6AKAqxs//rbbcdoyn3YQq1x6FVUUd39zzpezZntg9Naw==
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu@12.1.2":
|
"@next/swc-linux-arm64-musl@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.2.tgz#69764ffaacb3b9b373897fff15d7dd871455efe2"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.2.tgz#270db73e07a18d999f61e79a917943fa5bc1ef56"
|
||||||
integrity sha512-5N4QSRT60ikQqCU8iHfYZzlhg6MFTLsKhMTARmhn8wLtZfN9VVyTFwZrJQWjV64dZc4JFeXDANGao8fm55y6bw==
|
integrity sha512-rIZRFxI9N/502auJT1i7coas0HTHUM+HaXMyJiCpnY8Rimbo0495ir24tzzHo3nQqJwcflcPTwEh/DV17sdv9A==
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl@12.1.2":
|
"@next/swc-linux-x64-gnu@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.2.tgz#0ddaedb5ec578c01771f83be2046dafb2f70df91"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.2.tgz#e6c72fa20478552e898c434f4d4c0c5e89d2ea78"
|
||||||
integrity sha512-b32F/xAgdYG4Pt0foFzhF+2uhvNxnEj7aJNp1R4EhZotdej2PzvFWcP/dGkc7MJl205pBz5oC3gHyILIIlW6XA==
|
integrity sha512-ir1vNadlUDj7eQk15AvfhG5BjVizuCHks9uZwBfUgT5jyeDCeRvaDCo1+Q6+0CLOAnYDR/nqSCvBgzG2UdFh9A==
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc@12.1.2":
|
"@next/swc-linux-x64-musl@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.2.tgz#9e17ed56d5621f8c6961193da3a0b155cea511c9"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.2.tgz#b9ef9efe2c401839cdefa5e70402386aafdce15a"
|
||||||
integrity sha512-hVOcGmWDeVwO00Aclopsj6MoYhfJl5zA4vjAai9KjgclQTFZa/DC0vQjgKAHHKGT5oMHgjiq/G7L6P1/UfwYnw==
|
integrity sha512-bte5n2GzLN3O8JdSFYWZzMgEgDHZmRz5wiispiiDssj4ik3l8E7wq/czNi8RmIF+ioj2sYVokUNa/ekLzrESWw==
|
||||||
|
|
||||||
"@next/swc-win32-ia32-msvc@12.1.2":
|
"@next/swc-win32-arm64-msvc@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.2.tgz#ddd260cbe8bc4002fb54415b80baccf37f8db783"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.2.tgz#18fa7ec7248da3a7926a0601d9ececc53ac83157"
|
||||||
integrity sha512-wnVDGIVz2pR3vIkyN6IE+1NvMSBrBj1jba11iR16m8TAPzZH/PrNsxr0a9N5VavEXXLcQpoUVvT+N7nflbRAHg==
|
integrity sha512-ZUGCmcDmdPVSAlwJ/aD+1F9lYW8vttseiv4n2+VCDv5JloxiX9aY32kYZaJJO7hmTLNrprvXkb4OvNuHdN22Jg==
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc@12.1.2":
|
"@next/swc-win32-ia32-msvc@12.2.2":
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.2.tgz#37412a314bcf4c6006a74e1ef9764048344f3848"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.2.tgz#54936e84f4a219441d051940354da7cd3eafbb4f"
|
||||||
integrity sha512-MLNcurEpQp0+7OU9261f7PkN52xTGkfrt4IYTIXau7DO/aHj927oK6piIJdl9EOHdX/KN5W6qlyErj170PSHtw==
|
integrity sha512-v7ykeEDbr9eXiblGSZiEYYkWoig6sRhAbLKHUHQtk8vEWWVEqeXFcxmw6LRrKu5rCN1DY357UlYWToCGPQPCRA==
|
||||||
|
|
||||||
|
"@next/swc-win32-x64-msvc@12.2.2":
|
||||||
|
version "12.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.2.tgz#7460be700a60d75816f01109400b51fe929d7e89"
|
||||||
|
integrity sha512-2D2iinWUL6xx8D9LYVZ5qi7FP6uLAoWymt8m8aaG2Ld/Ka8/k723fJfiklfuAcwOxfufPJI+nRbT5VcgHGzHAQ==
|
||||||
|
|
||||||
"@nivo/annotations@0.74.0":
|
"@nivo/annotations@0.74.0":
|
||||||
version "0.74.0"
|
version "0.74.0"
|
||||||
|
@ -2837,6 +2842,13 @@
|
||||||
"@svgr/plugin-jsx" "^6.2.1"
|
"@svgr/plugin-jsx" "^6.2.1"
|
||||||
"@svgr/plugin-svgo" "^6.2.0"
|
"@svgr/plugin-svgo" "^6.2.0"
|
||||||
|
|
||||||
|
"@swc/helpers@0.4.2":
|
||||||
|
version "0.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.2.tgz#ed1f6997ffbc22396665d9ba74e2a5c0a2d782f8"
|
||||||
|
integrity sha512-556Az0VX7WR6UdoTn4htt/l3zPQ7bsQWK+HqdG4swV7beUCxo/BqmvbOpUkTIm/9ih86LIf1qsUnywNL3obGHw==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@szmarczak/http-timer@^1.1.2":
|
"@szmarczak/http-timer@^1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
|
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
|
||||||
|
@ -4290,7 +4302,7 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001335:
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz#8a1e7fdc4db9c2ec79a05e9fd68eb93a761888bb"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz#8a1e7fdc4db9c2ec79a05e9fd68eb93a761888bb"
|
||||||
integrity sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g==
|
integrity sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001230, caniuse-lite@^1.0.30001283, caniuse-lite@^1.0.30001332:
|
caniuse-lite@^1.0.30001230, caniuse-lite@^1.0.30001332:
|
||||||
version "1.0.30001341"
|
version "1.0.30001341"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz#59590c8ffa8b5939cf4161f00827b8873ad72498"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz#59590c8ffa8b5939cf4161f00827b8873ad72498"
|
||||||
integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA==
|
integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA==
|
||||||
|
@ -8320,29 +8332,31 @@ next-sitemap@^2.5.14:
|
||||||
"@corex/deepmerge" "^2.6.148"
|
"@corex/deepmerge" "^2.6.148"
|
||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
|
|
||||||
next@12.1.2:
|
next@12.2.2:
|
||||||
version "12.1.2"
|
version "12.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-12.1.2.tgz#c5376a8ae17d3e404a2b691c01f94c8943306f29"
|
resolved "https://registry.yarnpkg.com/next/-/next-12.2.2.tgz#029bf5e4a18a891ca5d05b189b7cd983fd22c072"
|
||||||
integrity sha512-JHPCsnFTBO0Z4SQxSYc611UA1WA+r/3y3Neg66AH5/gSO/oksfRnFw/zGX/FZ9+oOUHS9y3wJFawNpVYR2gJSQ==
|
integrity sha512-zAYFY45aBry/PlKONqtlloRFqU/We3zWYdn2NoGvDZkoYUYQSJC8WMcalS5C19MxbCZLUVCX7D7a6gTGgl2yLg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@next/env" "12.1.2"
|
"@next/env" "12.2.2"
|
||||||
caniuse-lite "^1.0.30001283"
|
"@swc/helpers" "0.4.2"
|
||||||
|
caniuse-lite "^1.0.30001332"
|
||||||
postcss "8.4.5"
|
postcss "8.4.5"
|
||||||
styled-jsx "5.0.1"
|
styled-jsx "5.0.2"
|
||||||
use-subscription "1.5.1"
|
use-sync-external-store "1.1.0"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@next/swc-android-arm-eabi" "12.1.2"
|
"@next/swc-android-arm-eabi" "12.2.2"
|
||||||
"@next/swc-android-arm64" "12.1.2"
|
"@next/swc-android-arm64" "12.2.2"
|
||||||
"@next/swc-darwin-arm64" "12.1.2"
|
"@next/swc-darwin-arm64" "12.2.2"
|
||||||
"@next/swc-darwin-x64" "12.1.2"
|
"@next/swc-darwin-x64" "12.2.2"
|
||||||
"@next/swc-linux-arm-gnueabihf" "12.1.2"
|
"@next/swc-freebsd-x64" "12.2.2"
|
||||||
"@next/swc-linux-arm64-gnu" "12.1.2"
|
"@next/swc-linux-arm-gnueabihf" "12.2.2"
|
||||||
"@next/swc-linux-arm64-musl" "12.1.2"
|
"@next/swc-linux-arm64-gnu" "12.2.2"
|
||||||
"@next/swc-linux-x64-gnu" "12.1.2"
|
"@next/swc-linux-arm64-musl" "12.2.2"
|
||||||
"@next/swc-linux-x64-musl" "12.1.2"
|
"@next/swc-linux-x64-gnu" "12.2.2"
|
||||||
"@next/swc-win32-arm64-msvc" "12.1.2"
|
"@next/swc-linux-x64-musl" "12.2.2"
|
||||||
"@next/swc-win32-ia32-msvc" "12.1.2"
|
"@next/swc-win32-arm64-msvc" "12.2.2"
|
||||||
"@next/swc-win32-x64-msvc" "12.1.2"
|
"@next/swc-win32-ia32-msvc" "12.2.2"
|
||||||
|
"@next/swc-win32-x64-msvc" "12.2.2"
|
||||||
|
|
||||||
no-case@^3.0.4:
|
no-case@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
|
@ -10892,10 +10906,10 @@ style-to-object@0.3.0, style-to-object@^0.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
inline-style-parser "0.1.1"
|
inline-style-parser "0.1.1"
|
||||||
|
|
||||||
styled-jsx@5.0.1:
|
styled-jsx@5.0.2:
|
||||||
version "5.0.1"
|
version "5.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.1.tgz#78fecbbad2bf95ce6cd981a08918ce4696f5fc80"
|
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.2.tgz#ff230fd593b737e9e68b630a694d460425478729"
|
||||||
integrity sha512-+PIZ/6Uk40mphiQJJI1202b+/dYeTVd9ZnMPR80pgiWbjIwvN2zIp4r9et0BgqBuShh48I0gttPlAXA7WVvBxw==
|
integrity sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ==
|
||||||
|
|
||||||
stylehacks@^5.1.0:
|
stylehacks@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
|
@ -11437,12 +11451,10 @@ use-latest@^1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
use-isomorphic-layout-effect "^1.1.1"
|
use-isomorphic-layout-effect "^1.1.1"
|
||||||
|
|
||||||
use-subscription@1.5.1:
|
use-sync-external-store@1.1.0:
|
||||||
version "1.5.1"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82"
|
||||||
integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA==
|
integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==
|
||||||
dependencies:
|
|
||||||
object-assign "^4.1.1"
|
|
||||||
|
|
||||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user