Merge branch 'main' into inga/manalink-table-replacement
This commit is contained in:
commit
976b294869
|
@ -1,6 +1,7 @@
|
|||
import { difference } from 'lodash'
|
||||
|
||||
export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default'
|
||||
|
||||
export const CATEGORIES = {
|
||||
politics: 'Politics',
|
||||
technology: 'Technology',
|
||||
|
@ -37,3 +38,8 @@ export const EXCLUDED_CATEGORIES: category[] = [
|
|||
]
|
||||
|
||||
export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES)
|
||||
|
||||
export const DEFAULT_CATEGORY_GROUPS = DEFAULT_CATEGORIES.map((c) => ({
|
||||
slug: c.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX,
|
||||
name: CATEGORIES[c as category],
|
||||
}))
|
||||
|
|
|
@ -48,6 +48,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
groupSlugs?: string[]
|
||||
uniqueBettorIds?: string[]
|
||||
uniqueBettorCount?: number
|
||||
popularityScore?: number
|
||||
} & T
|
||||
|
||||
export type BinaryContract = Contract & Binary
|
||||
|
|
|
@ -22,6 +22,7 @@ export * from './on-update-user'
|
|||
export * from './on-create-comment-on-group'
|
||||
export * from './on-create-txn'
|
||||
export * from './on-delete-group'
|
||||
export * from './score-contracts'
|
||||
|
||||
// v2
|
||||
export * from './health'
|
||||
|
|
54
functions/src/score-contracts.ts
Normal file
54
functions/src/score-contracts.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { Bet } from 'common/bet'
|
||||
import { uniq } from 'lodash'
|
||||
import { Contract } from 'common/contract'
|
||||
import { log } from './utils'
|
||||
|
||||
export const scoreContracts = functions.pubsub
|
||||
.schedule('every 1 hours')
|
||||
.onRun(async () => {
|
||||
await scoreContractsInternal()
|
||||
})
|
||||
const firestore = admin.firestore()
|
||||
|
||||
async function scoreContractsInternal() {
|
||||
const now = Date.now()
|
||||
const lastHour = now - 60 * 60 * 1000
|
||||
const last3Days = now - 1000 * 60 * 60 * 24 * 3
|
||||
const activeContractsSnap = await firestore
|
||||
.collection('contracts')
|
||||
.where('lastUpdatedTime', '>', lastHour)
|
||||
.get()
|
||||
const activeContracts = activeContractsSnap.docs.map(
|
||||
(doc) => doc.data() as Contract
|
||||
)
|
||||
// We have to downgrade previously active contracts to allow the new ones to bubble up
|
||||
const previouslyActiveContractsSnap = await firestore
|
||||
.collection('contracts')
|
||||
.where('popularityScore', '>', 0)
|
||||
.get()
|
||||
const activeContractIds = activeContracts.map((c) => c.id)
|
||||
const previouslyActiveContracts = previouslyActiveContractsSnap.docs
|
||||
.map((doc) => doc.data() as Contract)
|
||||
.filter((c) => !activeContractIds.includes(c.id))
|
||||
|
||||
const contracts = activeContracts.concat(previouslyActiveContracts)
|
||||
log(`Found ${contracts.length} contracts to score`)
|
||||
|
||||
for (const contract of contracts) {
|
||||
const bets = await firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.where('createdTime', '>', last3Days)
|
||||
.get()
|
||||
const bettors = bets.docs
|
||||
.map((doc) => doc.data() as Bet)
|
||||
.map((bet) => bet.userId)
|
||||
const score = uniq(bettors).length
|
||||
if (contract.popularityScore !== score)
|
||||
await firestore
|
||||
.collection('contracts')
|
||||
.doc(contract.id)
|
||||
.update({ popularityScore: score })
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
# Ignore Next artifacts
|
||||
.next/
|
||||
out/
|
||||
public/**/*.json
|
210
web/components/NotificationSettings.tsx
Normal file
210
web/components/NotificationSettings.tsx
Normal file
|
@ -0,0 +1,210 @@
|
|||
import { useUser } from 'web/hooks/use-user'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { notification_subscribe_types, PrivateUser } from 'common/user'
|
||||
import { listenForPrivateUser, updatePrivateUser } from 'web/lib/firebase/users'
|
||||
import toast from 'react-hot-toast'
|
||||
import { track } from '@amplitude/analytics-browser'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import clsx from 'clsx'
|
||||
import { CheckIcon, XIcon } from '@heroicons/react/outline'
|
||||
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||
|
||||
export function NotificationSettings() {
|
||||
const user = useUser()
|
||||
const [notificationSettings, setNotificationSettings] =
|
||||
useState<notification_subscribe_types>('all')
|
||||
const [emailNotificationSettings, setEmailNotificationSettings] =
|
||||
useState<notification_subscribe_types>('all')
|
||||
const [privateUser, setPrivateUser] = useState<PrivateUser | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (user) listenForPrivateUser(user.id, setPrivateUser)
|
||||
}, [user])
|
||||
|
||||
useEffect(() => {
|
||||
if (!privateUser) return
|
||||
if (privateUser.notificationPreferences) {
|
||||
setNotificationSettings(privateUser.notificationPreferences)
|
||||
}
|
||||
if (
|
||||
privateUser.unsubscribedFromResolutionEmails &&
|
||||
privateUser.unsubscribedFromCommentEmails &&
|
||||
privateUser.unsubscribedFromAnswerEmails
|
||||
) {
|
||||
setEmailNotificationSettings('none')
|
||||
} else if (
|
||||
!privateUser.unsubscribedFromResolutionEmails &&
|
||||
!privateUser.unsubscribedFromCommentEmails &&
|
||||
!privateUser.unsubscribedFromAnswerEmails
|
||||
) {
|
||||
setEmailNotificationSettings('all')
|
||||
} else {
|
||||
setEmailNotificationSettings('less')
|
||||
}
|
||||
}, [privateUser])
|
||||
|
||||
const loading = 'Changing Notifications Settings'
|
||||
const success = 'Notification Settings Changed!'
|
||||
function changeEmailNotifications(newValue: notification_subscribe_types) {
|
||||
if (!privateUser) return
|
||||
if (newValue === 'all') {
|
||||
toast.promise(
|
||||
updatePrivateUser(privateUser.id, {
|
||||
unsubscribedFromResolutionEmails: false,
|
||||
unsubscribedFromCommentEmails: false,
|
||||
unsubscribedFromAnswerEmails: false,
|
||||
}),
|
||||
{
|
||||
loading,
|
||||
success,
|
||||
error: (err) => `${err.message}`,
|
||||
}
|
||||
)
|
||||
} else if (newValue === 'less') {
|
||||
toast.promise(
|
||||
updatePrivateUser(privateUser.id, {
|
||||
unsubscribedFromResolutionEmails: false,
|
||||
unsubscribedFromCommentEmails: true,
|
||||
unsubscribedFromAnswerEmails: true,
|
||||
}),
|
||||
{
|
||||
loading,
|
||||
success,
|
||||
error: (err) => `${err.message}`,
|
||||
}
|
||||
)
|
||||
} else if (newValue === 'none') {
|
||||
toast.promise(
|
||||
updatePrivateUser(privateUser.id, {
|
||||
unsubscribedFromResolutionEmails: true,
|
||||
unsubscribedFromCommentEmails: true,
|
||||
unsubscribedFromAnswerEmails: true,
|
||||
}),
|
||||
{
|
||||
loading,
|
||||
success,
|
||||
error: (err) => `${err.message}`,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function changeInAppNotificationSettings(
|
||||
newValue: notification_subscribe_types
|
||||
) {
|
||||
if (!privateUser) return
|
||||
track('In-App Notification Preferences Changed', {
|
||||
newPreference: newValue,
|
||||
oldPreference: privateUser.notificationPreferences,
|
||||
})
|
||||
toast.promise(
|
||||
updatePrivateUser(privateUser.id, {
|
||||
notificationPreferences: newValue,
|
||||
}),
|
||||
{
|
||||
loading,
|
||||
success,
|
||||
error: (err) => `${err.message}`,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (privateUser && privateUser.notificationPreferences)
|
||||
setNotificationSettings(privateUser.notificationPreferences)
|
||||
else setNotificationSettings('all')
|
||||
}, [privateUser])
|
||||
|
||||
if (!privateUser) {
|
||||
return <LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} />
|
||||
}
|
||||
|
||||
function NotificationSettingLine(props: {
|
||||
label: string
|
||||
highlight: boolean
|
||||
}) {
|
||||
const { label, highlight } = props
|
||||
return (
|
||||
<Row className={clsx('my-1 text-gray-300', highlight && '!text-black')}>
|
||||
{highlight ? <CheckIcon height={20} /> : <XIcon height={20} />}
|
||||
{label}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'p-2'}>
|
||||
<div>In App Notifications</div>
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={notificationSettings}
|
||||
choicesMap={{ All: 'all', Less: 'less', None: 'none' }}
|
||||
setChoice={(choice) =>
|
||||
changeInAppNotificationSettings(
|
||||
choice as notification_subscribe_types
|
||||
)
|
||||
}
|
||||
className={'col-span-4 p-2'}
|
||||
toggleClassName={'w-24'}
|
||||
/>
|
||||
<div className={'mt-4 text-sm'}>
|
||||
<div>
|
||||
<div className={''}>
|
||||
You will receive notifications for:
|
||||
<NotificationSettingLine
|
||||
label={"Resolution of questions you've interacted with"}
|
||||
highlight={notificationSettings !== 'none'}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
highlight={notificationSettings !== 'none'}
|
||||
label={'Activity on your own questions, comments, & answers'}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
highlight={notificationSettings !== 'none'}
|
||||
label={"Activity on questions you're betting on"}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
highlight={notificationSettings !== 'none'}
|
||||
label={"Income & referral bonuses you've received"}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
label={"Activity on questions you've ever bet or commented on"}
|
||||
highlight={notificationSettings === 'all'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'mt-4'}>Email Notifications</div>
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={emailNotificationSettings}
|
||||
choicesMap={{ All: 'all', Less: 'less', None: 'none' }}
|
||||
setChoice={(choice) =>
|
||||
changeEmailNotifications(choice as notification_subscribe_types)
|
||||
}
|
||||
className={'col-span-4 p-2'}
|
||||
toggleClassName={'w-24'}
|
||||
/>
|
||||
<div className={'mt-4 text-sm'}>
|
||||
<div>
|
||||
You will receive emails for:
|
||||
<NotificationSettingLine
|
||||
label={"Resolution of questions you're betting on"}
|
||||
highlight={emailNotificationSettings !== 'none'}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
label={'Closure of your questions'}
|
||||
highlight={emailNotificationSettings !== 'none'}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
label={'Activity on your questions'}
|
||||
highlight={emailNotificationSettings === 'all'}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
label={"Activity on questions you've answered or commented on"}
|
||||
highlight={emailNotificationSettings === 'all'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -50,7 +50,7 @@ import { LimitOrderTable } from './limit-bets'
|
|||
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
||||
type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all'
|
||||
|
||||
const CONTRACTS_PER_PAGE = 20
|
||||
const CONTRACTS_PER_PAGE = 50
|
||||
|
||||
export function BetsList(props: {
|
||||
user: User
|
||||
|
|
|
@ -13,7 +13,7 @@ export function PillButton(props: {
|
|||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'cursor-pointer select-none rounded-full',
|
||||
'cursor-pointer select-none whitespace-nowrap rounded-full',
|
||||
selected
|
||||
? ['text-white', color ?? 'bg-gray-700']
|
||||
: 'bg-gray-100 hover:bg-gray-200',
|
||||
|
|
|
@ -25,9 +25,10 @@ import { useFollows } from 'web/hooks/use-follows'
|
|||
import { trackCallback } from 'web/lib/service/analytics'
|
||||
import ContractSearchFirestore from 'web/pages/contract-search-firestore'
|
||||
import { useMemberGroups } from 'web/hooks/use-group'
|
||||
import { NEW_USER_GROUP_SLUGS } from 'common/group'
|
||||
import { Group, NEW_USER_GROUP_SLUGS } from 'common/group'
|
||||
import { PillButton } from './buttons/pill-button'
|
||||
import { toPairs } from 'lodash'
|
||||
import { sortBy } from 'lodash'
|
||||
import { DEFAULT_CATEGORY_GROUPS } from 'common/categories'
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
'GJQPAYENIF',
|
||||
|
@ -39,22 +40,16 @@ const indexPrefix = ENV === 'DEV' ? 'dev-' : ''
|
|||
const sortIndexes = [
|
||||
{ label: 'Newest', value: indexPrefix + 'contracts-newest' },
|
||||
{ label: 'Oldest', value: indexPrefix + 'contracts-oldest' },
|
||||
{ label: 'Most popular', value: indexPrefix + 'contracts-most-popular' },
|
||||
{ label: 'Most popular', value: indexPrefix + 'contracts-score' },
|
||||
{ label: 'Most traded', value: indexPrefix + 'contracts-most-traded' },
|
||||
{ label: '24h volume', value: indexPrefix + 'contracts-24-hour-vol' },
|
||||
{ label: 'Last updated', value: indexPrefix + 'contracts-last-updated' },
|
||||
{ label: 'Close date', value: indexPrefix + 'contracts-close-date' },
|
||||
{ label: 'Resolve date', value: indexPrefix + 'contracts-resolve-date' },
|
||||
]
|
||||
export const DEFAULT_SORT = 'score'
|
||||
|
||||
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
|
||||
const filterOptions: { [label: string]: filter } = {
|
||||
All: 'all',
|
||||
Open: 'open',
|
||||
Closed: 'closed',
|
||||
Resolved: 'resolved',
|
||||
'For you': 'personal',
|
||||
}
|
||||
|
||||
export function ContractSearch(props: {
|
||||
querySortOptions?: {
|
||||
|
@ -85,9 +80,24 @@ export function ContractSearch(props: {
|
|||
} = props
|
||||
|
||||
const user = useUser()
|
||||
const memberGroupSlugs = useMemberGroups(user?.id)
|
||||
?.map((g) => g.slug)
|
||||
.filter((s) => !NEW_USER_GROUP_SLUGS.includes(s))
|
||||
const memberGroups = (useMemberGroups(user?.id) ?? []).filter(
|
||||
(group) => !NEW_USER_GROUP_SLUGS.includes(group.slug)
|
||||
)
|
||||
const memberGroupSlugs =
|
||||
memberGroups.length > 0
|
||||
? memberGroups.map((g) => g.slug)
|
||||
: DEFAULT_CATEGORY_GROUPS.map((g) => g.slug)
|
||||
|
||||
const memberPillGroups = sortBy(
|
||||
memberGroups.filter((group) => group.contractIds.length > 0),
|
||||
(group) => group.contractIds.length
|
||||
).reverse()
|
||||
|
||||
const defaultPillGroups = DEFAULT_CATEGORY_GROUPS as Group[]
|
||||
|
||||
const pillGroups =
|
||||
memberPillGroups.length > 0 ? memberPillGroups : defaultPillGroups
|
||||
|
||||
const follows = useFollows(user?.id)
|
||||
const { initialSort } = useInitialQueryAndSort(querySortOptions)
|
||||
|
||||
|
@ -95,29 +105,19 @@ export function ContractSearch(props: {
|
|||
.map(({ value }) => value)
|
||||
.includes(`${indexPrefix}contracts-${initialSort ?? ''}`)
|
||||
? initialSort
|
||||
: querySortOptions?.defaultSort ?? 'most-popular'
|
||||
: querySortOptions?.defaultSort ?? DEFAULT_SORT
|
||||
|
||||
const [filter, setFilter] = useState<filter>(
|
||||
querySortOptions?.defaultFilter ?? 'open'
|
||||
)
|
||||
const pillsEnabled = !additionalFilter
|
||||
const [pillFilter, setPillFilter] = useState<string | undefined>(undefined)
|
||||
|
||||
const { filters, numericFilters } = useMemo(() => {
|
||||
let filters = [
|
||||
filter === 'open' ? 'isResolved:false' : '',
|
||||
filter === 'closed' ? 'isResolved:false' : '',
|
||||
filter === 'resolved' ? 'isResolved:true' : '',
|
||||
filter === 'personal'
|
||||
? // Show contracts in groups that the user is a member of
|
||||
(memberGroupSlugs?.map((slug) => `groupSlugs:${slug}`) ?? [])
|
||||
// Show contracts created by users the user follows
|
||||
.concat(follows?.map((followId) => `creatorId:${followId}`) ?? [])
|
||||
// Show contracts bet on by users the user follows
|
||||
.concat(
|
||||
follows?.map((followId) => `uniqueBettorIds:${followId}`) ?? []
|
||||
// Show contracts bet on by the user
|
||||
)
|
||||
.concat(user ? `uniqueBettorIds:${user.id}` : [])
|
||||
: '',
|
||||
additionalFilter?.creatorId
|
||||
? `creatorId:${additionalFilter.creatorId}`
|
||||
: '',
|
||||
|
@ -125,6 +125,26 @@ export function ContractSearch(props: {
|
|||
additionalFilter?.groupSlug
|
||||
? `groupSlugs:${additionalFilter.groupSlug}`
|
||||
: '',
|
||||
pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets'
|
||||
? `groupSlugs:${pillFilter}`
|
||||
: '',
|
||||
pillFilter === 'personal'
|
||||
? // Show contracts in groups that the user is a member of
|
||||
memberGroupSlugs
|
||||
.map((slug) => `groupSlugs:${slug}`)
|
||||
// Show contracts created by users the user follows
|
||||
.concat(follows?.map((followId) => `creatorId:${followId}`) ?? [])
|
||||
// Show contracts bet on by users the user follows
|
||||
.concat(
|
||||
follows?.map((followId) => `uniqueBettorIds:${followId}`) ?? []
|
||||
)
|
||||
: '',
|
||||
// Subtract contracts you bet on from For you.
|
||||
pillFilter === 'personal' && user ? `uniqueBettorIds:-${user.id}` : '',
|
||||
pillFilter === 'your-bets' && user
|
||||
? // Show contracts bet on by the user
|
||||
`uniqueBettorIds:${user.id}`
|
||||
: '',
|
||||
].filter((f) => f)
|
||||
// Hack to make Algolia work.
|
||||
filters = ['', ...filters]
|
||||
|
@ -138,8 +158,9 @@ export function ContractSearch(props: {
|
|||
}, [
|
||||
filter,
|
||||
Object.values(additionalFilter ?? {}).join(','),
|
||||
(memberGroupSlugs ?? []).join(','),
|
||||
memberGroupSlugs.join(','),
|
||||
(follows ?? []).join(','),
|
||||
pillFilter,
|
||||
])
|
||||
|
||||
const indexName = `${indexPrefix}contracts-${sort}`
|
||||
|
@ -166,6 +187,17 @@ export function ContractSearch(props: {
|
|||
}}
|
||||
/>
|
||||
{/*// TODO track WHICH filter users are using*/}
|
||||
<select
|
||||
className="!select !select-bordered"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value as filter)}
|
||||
onBlur={trackCallback('select search filter')}
|
||||
>
|
||||
<option value="open">Open</option>
|
||||
<option value="closed">Closed</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
<option value="all">All</option>
|
||||
</select>
|
||||
{!hideOrderSelector && (
|
||||
<SortBy
|
||||
items={sortIndexes}
|
||||
|
@ -185,25 +217,50 @@ export function ContractSearch(props: {
|
|||
|
||||
<Spacer h={3} />
|
||||
|
||||
<Row className="gap-2">
|
||||
{toPairs<filter>(filterOptions).map(([label, f]) => {
|
||||
{pillsEnabled && (
|
||||
<Row className="scrollbar-hide items-start gap-2 overflow-x-auto">
|
||||
<PillButton
|
||||
key={'all'}
|
||||
selected={pillFilter === undefined}
|
||||
onSelect={() => setPillFilter(undefined)}
|
||||
>
|
||||
All
|
||||
</PillButton>
|
||||
<PillButton
|
||||
key={'personal'}
|
||||
selected={pillFilter === 'personal'}
|
||||
onSelect={() => setPillFilter('personal')}
|
||||
>
|
||||
For you
|
||||
</PillButton>
|
||||
|
||||
<PillButton
|
||||
key={'your-bets'}
|
||||
selected={pillFilter === 'your-bets'}
|
||||
onSelect={() => setPillFilter('your-bets')}
|
||||
>
|
||||
Your bets
|
||||
</PillButton>
|
||||
|
||||
{pillGroups.map(({ name, slug }) => {
|
||||
return (
|
||||
<PillButton
|
||||
key={f}
|
||||
selected={filter === f}
|
||||
onSelect={() => setFilter(f)}
|
||||
key={slug}
|
||||
selected={pillFilter === slug}
|
||||
onSelect={() => setPillFilter(slug)}
|
||||
>
|
||||
{label}
|
||||
{name}
|
||||
</PillButton>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Spacer h={3} />
|
||||
|
||||
{filter === 'personal' &&
|
||||
(follows ?? []).length === 0 &&
|
||||
(memberGroupSlugs ?? []).length === 0 ? (
|
||||
memberGroupSlugs.length === 0 ? (
|
||||
<>You're not following anyone, nor in any of your own groups yet.</>
|
||||
) : (
|
||||
<ContractSearchInner
|
||||
|
|
|
@ -16,7 +16,6 @@ import { ShareEmbedButton } from '../share-embed-button'
|
|||
import { Title } from '../title'
|
||||
import { TweetButton } from '../tweet-button'
|
||||
import { InfoTooltip } from '../info-tooltip'
|
||||
import { TagsInput } from 'web/components/tags-input'
|
||||
import { DuplicateContractButton } from '../copy-contract-button'
|
||||
|
||||
export const contractDetailsButtonClassName =
|
||||
|
@ -141,9 +140,6 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<div>Tags</div>
|
||||
<TagsInput contract={contract} />
|
||||
<div />
|
||||
{contract.mechanism === 'cpmm-1' && !contract.resolution && (
|
||||
<LiquidityPanel contract={contract} />
|
||||
)}
|
||||
|
|
|
@ -150,7 +150,8 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
|||
enableSlices="x"
|
||||
enableGridX={!!width && width >= 800}
|
||||
enableArea
|
||||
margin={{ top: 20, right: 20, bottom: 25, left: 40 }}
|
||||
areaBaselineValue={isBinary || isLogScale ? 0 : contract.min}
|
||||
margin={{ top: 20, right: 20, bottom: 65, left: 40 }}
|
||||
animate={false}
|
||||
sliceTooltip={SliceTooltip}
|
||||
/>
|
||||
|
|
|
@ -3,31 +3,35 @@ import { LinkIcon } from '@heroicons/react/outline'
|
|||
import { Menu, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Contract } from 'common/contract'
|
||||
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 { track } from 'web/lib/service/analytics'
|
||||
|
||||
function copyContractUrl(contract: Contract) {
|
||||
copyToClipboard(`https://${ENV_CONFIG.domain}${contractPath(contract)}`)
|
||||
}
|
||||
import { Row } from './layout/row'
|
||||
|
||||
export function CopyLinkButton(props: {
|
||||
contract: Contract
|
||||
url: string
|
||||
displayUrl?: string
|
||||
tracking?: string
|
||||
buttonClassName?: string
|
||||
toastClassName?: string
|
||||
}) {
|
||||
const { contract, buttonClassName, toastClassName } = props
|
||||
const { url, displayUrl, tracking, buttonClassName, toastClassName } = props
|
||||
|
||||
return (
|
||||
<Row className="w-full">
|
||||
<input
|
||||
className="input input-bordered flex-1 rounded-r-none text-gray-500"
|
||||
readOnly
|
||||
type="text"
|
||||
value={displayUrl ?? url}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
as="div"
|
||||
className="relative z-10 flex-shrink-0"
|
||||
onMouseUp={() => {
|
||||
copyContractUrl(contract)
|
||||
track('copy share link')
|
||||
copyToClipboard(url)
|
||||
track(tracking ?? 'copy share link')
|
||||
}}
|
||||
>
|
||||
<Menu.Button
|
||||
|
@ -56,5 +60,6 @@ export function CopyLinkButton(props: {
|
|||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import React from 'react'
|
|||
|
||||
export const createButtonStyle =
|
||||
'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0 h-11'
|
||||
|
||||
export const CreateQuestionButton = (props: {
|
||||
user: User | null | undefined
|
||||
overrideText?: string
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Answer } from 'common/answer'
|
||||
import { Bet } from 'common/bet'
|
||||
import { Comment } from 'common/comment'
|
||||
import { formatPercent } from 'common/util/format'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { Modal } from 'web/components/layout/modal'
|
||||
|
@ -11,8 +10,6 @@ import { Avatar } from 'web/components/avatar'
|
|||
import { UserLink } from 'web/components/user-page'
|
||||
import { Linkify } from 'web/components/linkify'
|
||||
import clsx from 'clsx'
|
||||
import { tradingAllowed } from 'web/lib/firebase/contracts'
|
||||
import { BuyButton } from 'web/components/yes-no-selector'
|
||||
import {
|
||||
CommentInput,
|
||||
CommentRepliesList,
|
||||
|
@ -23,7 +20,6 @@ import { useRouter } from 'next/router'
|
|||
import { groupBy } from 'lodash'
|
||||
import { User } from 'common/user'
|
||||
import { useEvent } from 'web/hooks/use-event'
|
||||
import { getDpmOutcomeProbability } from 'common/calculate-dpm'
|
||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||
|
||||
export function FeedAnswerCommentGroup(props: {
|
||||
|
@ -50,11 +46,6 @@ export function FeedAnswerCommentGroup(props: {
|
|||
const commentsList = comments.filter(
|
||||
(comment) => comment.answerOutcome === answer.number.toString()
|
||||
)
|
||||
const thisAnswerProb = getDpmOutcomeProbability(
|
||||
contract.totalShares,
|
||||
answer.id
|
||||
)
|
||||
const probPercent = formatPercent(thisAnswerProb)
|
||||
const betsByCurrentUser = (user && betsByUserId[user.id]) ?? []
|
||||
const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? []
|
||||
const isFreeResponseContractPage = !!commentsByCurrentUser
|
||||
|
@ -112,7 +103,7 @@ export function FeedAnswerCommentGroup(props: {
|
|||
}, [answerElementId, router.asPath])
|
||||
|
||||
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}
|
||||
|
@ -125,7 +116,7 @@ export function FeedAnswerCommentGroup(props: {
|
|||
|
||||
<Row
|
||||
className={clsx(
|
||||
'my-4 flex gap-3 space-x-3 transition-all duration-1000',
|
||||
'mt-4 flex gap-3 space-x-3 transition-all duration-1000',
|
||||
highlighted ? `-m-2 my-3 rounded bg-indigo-500/[0.2] p-2` : ''
|
||||
)}
|
||||
id={answerElementId}
|
||||
|
@ -162,24 +153,6 @@ export function FeedAnswerCommentGroup(props: {
|
|||
</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}
|
||||
</span>
|
||||
<BuyButton
|
||||
className={clsx(
|
||||
'btn-sm flex-initial !px-6 sm:flex',
|
||||
tradingAllowed(contract) ? '' : '!hidden'
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
</Row>
|
||||
</Col>
|
||||
{isFreeResponseContractPage && (
|
||||
|
|
|
@ -93,6 +93,24 @@ export function BetStatusText(props: {
|
|||
bet.fills?.some((fill) => fill.matchedBetId === null)) ??
|
||||
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 (
|
||||
<div className="text-sm text-gray-500">
|
||||
{bettor ? (
|
||||
|
@ -112,14 +130,9 @@ export function BetStatusText(props: {
|
|||
contract={contract}
|
||||
truncate="short"
|
||||
/>{' '}
|
||||
{isPseudoNumeric
|
||||
? ' than ' + formatNumericProbability(bet.probAfter, contract)
|
||||
: ' at ' +
|
||||
formatPercent(
|
||||
hadPoolMatch || isFreeResponse
|
||||
? bet.probAfter
|
||||
: bet.limitProb ?? bet.probAfter
|
||||
)}
|
||||
{fromProb === toProb
|
||||
? `at ${fromProb}`
|
||||
: `from ${fromProb} to ${toProb}`}
|
||||
</>
|
||||
)}
|
||||
<RelativeTimestamp time={createdTime} />
|
||||
|
|
|
@ -142,7 +142,7 @@ export function CommentRepliesList(props: {
|
|||
id={comment.id}
|
||||
className={clsx(
|
||||
'relative',
|
||||
!treatFirstIndexEqually && commentIdx === 0 ? '' : 'mt-3 ml-6'
|
||||
!treatFirstIndexEqually && commentIdx === 0 ? '' : 'ml-6'
|
||||
)}
|
||||
>
|
||||
{/*draw a gray line from the comment to the left:*/}
|
||||
|
|
|
@ -76,11 +76,13 @@ export function LimitOrderTable(props: {
|
|||
return (
|
||||
<table className="table-compact table w-full rounded text-gray-500">
|
||||
<thead>
|
||||
<tr>
|
||||
{!isYou && <th></th>}
|
||||
<th>Outcome</th>
|
||||
<th>{isPseudoNumeric ? 'Value' : 'Prob'}</th>
|
||||
<th>Amount</th>
|
||||
{isYou && <th></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{limitBets.map((bet) => (
|
||||
|
|
|
@ -235,7 +235,10 @@ export default function Sidebar(props: { className?: string }) {
|
|||
buttonContent={<MoreButton />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Spacer if there are any groups */}
|
||||
{memberItems.length > 0 && (
|
||||
<hr className="!my-4 mr-2 border-gray-300" />
|
||||
)}
|
||||
{privateUser && (
|
||||
<GroupsList
|
||||
currentPage={router.asPath}
|
||||
|
@ -256,11 +259,7 @@ export default function Sidebar(props: { className?: string }) {
|
|||
/>
|
||||
|
||||
{/* Spacer if there are any groups */}
|
||||
{memberItems.length > 0 && (
|
||||
<div className="py-3">
|
||||
<div className="h-[1px] bg-gray-300" />
|
||||
</div>
|
||||
)}
|
||||
{memberItems.length > 0 && <hr className="!my-4 border-gray-300" />}
|
||||
{privateUser && (
|
||||
<GroupsList
|
||||
currentPage={router.asPath}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import clsx from 'clsx'
|
||||
import { Contract, contractUrl } from 'web/lib/firebase/contracts'
|
||||
|
||||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
|
||||
import { Contract, contractPath, contractUrl } from 'web/lib/firebase/contracts'
|
||||
import { CopyLinkButton } from './copy-link-button'
|
||||
import { Col } from './layout/col'
|
||||
import { Row } from './layout/row'
|
||||
|
@ -7,18 +10,15 @@ import { Row } from './layout/row'
|
|||
export function ShareMarket(props: { contract: Contract; className?: string }) {
|
||||
const { contract, className } = props
|
||||
|
||||
const url = `https://${ENV_CONFIG.domain}${contractPath(contract)}`
|
||||
|
||||
return (
|
||||
<Col className={clsx(className, 'gap-3')}>
|
||||
<div>Share your market</div>
|
||||
<Row className="mb-6 items-center">
|
||||
<input
|
||||
className="input input-bordered flex-1 rounded-r-none text-gray-500"
|
||||
readOnly
|
||||
type="text"
|
||||
value={contractUrl(contract)}
|
||||
/>
|
||||
<CopyLinkButton
|
||||
contract={contract}
|
||||
url={url}
|
||||
displayUrl={contractUrl(contract)}
|
||||
buttonClassName="btn-md rounded-l-none"
|
||||
toastClassName={'-left-28 mt-1'}
|
||||
/>
|
||||
|
|
|
@ -39,6 +39,7 @@ import { PortfolioValueSection } from './portfolio/portfolio-value-section'
|
|||
import { filterDefined } from 'common/util/array'
|
||||
import { useUserBets } from 'web/hooks/use-user-bets'
|
||||
import { ReferralsButton } from 'web/components/referrals-button'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
|
||||
export function UserLink(props: {
|
||||
name: string
|
||||
|
@ -123,6 +124,7 @@ export function UserPage(props: {
|
|||
|
||||
const yourFollows = useFollows(currentUser?.id)
|
||||
const isFollowing = yourFollows?.includes(user.id)
|
||||
const profit = user.profitCached.allTime
|
||||
|
||||
const onFollow = () => {
|
||||
if (!currentUser) return
|
||||
|
@ -187,6 +189,17 @@ export function UserPage(props: {
|
|||
<Col className="mx-4 -mt-6">
|
||||
<span className="text-2xl font-bold">{user.name}</span>
|
||||
<span className="text-gray-500">@{user.username}</span>
|
||||
<span className="text-gray-500">
|
||||
<span
|
||||
className={clsx(
|
||||
'text-md',
|
||||
profit >= 0 ? 'text-green-600' : 'text-red-400'
|
||||
)}
|
||||
>
|
||||
{formatMoney(profit)}
|
||||
</span>{' '}
|
||||
profit
|
||||
</span>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useSearchBox } from 'react-instantsearch-hooks-web'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { DEFAULT_SORT } from 'web/components/contract-search'
|
||||
|
||||
const MARKETS_SORT = 'markets_sort'
|
||||
|
||||
|
@ -10,11 +11,11 @@ export type Sort =
|
|||
| 'newest'
|
||||
| 'oldest'
|
||||
| 'most-traded'
|
||||
| 'most-popular'
|
||||
| '24-hour-vol'
|
||||
| 'close-date'
|
||||
| 'resolve-date'
|
||||
| 'last-updated'
|
||||
| 'score'
|
||||
|
||||
export function getSavedSort() {
|
||||
// TODO: this obviously doesn't work with SSR, common sense would suggest
|
||||
|
@ -31,7 +32,7 @@ export function useInitialQueryAndSort(options?: {
|
|||
shouldLoadFromStorage?: boolean
|
||||
}) {
|
||||
const { defaultSort, shouldLoadFromStorage } = defaults(options, {
|
||||
defaultSort: 'most-popular',
|
||||
defaultSort: DEFAULT_SORT,
|
||||
shouldLoadFromStorage: true,
|
||||
})
|
||||
const router = useRouter()
|
||||
|
|
|
@ -1,32 +1,28 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { PrivateUser, User } from 'common/user'
|
||||
import {
|
||||
listenForAllUsers,
|
||||
listenForPrivateUsers,
|
||||
} from 'web/lib/firebase/users'
|
||||
import { groupBy, sortBy, difference } from 'lodash'
|
||||
import { getContractsOfUserBets } from 'web/lib/firebase/bets'
|
||||
import { useFollows } from './use-follows'
|
||||
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 = () => {
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
listenForAllUsers(setUsers)
|
||||
}, [])
|
||||
|
||||
return users
|
||||
const result = useFirestoreQueryData<DocumentData, User[]>(['users'], users, {
|
||||
subscribe: true,
|
||||
includeMetadataChanges: true,
|
||||
})
|
||||
return result.data ?? []
|
||||
}
|
||||
|
||||
export const usePrivateUsers = () => {
|
||||
const [users, setUsers] = useState<PrivateUser[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
listenForPrivateUsers(setUsers)
|
||||
}, [])
|
||||
|
||||
return users
|
||||
const result = useFirestoreQueryData<DocumentData, PrivateUser[]>(
|
||||
['private users'],
|
||||
privateUsers,
|
||||
{ subscribe: true, includeMetadataChanges: true }
|
||||
)
|
||||
return result.data || []
|
||||
}
|
||||
|
||||
export const useDiscoverUsers = (userId: string | null | undefined) => {
|
||||
|
|
|
@ -22,7 +22,12 @@ export const groups = coll<Group>('groups')
|
|||
|
||||
export function groupPath(
|
||||
groupSlug: string,
|
||||
subpath?: 'edit' | 'questions' | 'about' | typeof GROUP_CHAT_SLUG | 'rankings'
|
||||
subpath?:
|
||||
| 'edit'
|
||||
| 'questions'
|
||||
| 'about'
|
||||
| typeof GROUP_CHAT_SLUG
|
||||
| 'leaderboards'
|
||||
) {
|
||||
return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}`
|
||||
}
|
||||
|
|
|
@ -52,12 +52,19 @@ export const getServerAuthenticatedUid = async (ctx: RequestContext) => {
|
|||
if (idToken != null) {
|
||||
try {
|
||||
return (await auth.verifyIdToken(idToken))?.uid
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// plausibly expired; try the refresh token, if it's present
|
||||
}
|
||||
}
|
||||
if (refreshToken != null) {
|
||||
try {
|
||||
const resp = await requestFirebaseIdToken(refreshToken)
|
||||
setAuthCookies(resp.id_token, resp.refresh_token, ctx.res)
|
||||
return (await auth.verifyIdToken(resp.id_token))?.uid
|
||||
}
|
||||
} catch (e) {
|
||||
// this is a big unexpected problem -- either their cookies are corrupt
|
||||
// or the refresh token API is down. functionally, they are not logged in
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
|
|
|
@ -258,16 +258,6 @@ export async function listAllUsers() {
|
|||
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) {
|
||||
const topTraders = query(
|
||||
users,
|
||||
|
@ -275,7 +265,7 @@ export function getTopTraders(period: Period) {
|
|||
limit(20)
|
||||
)
|
||||
|
||||
return getValues(topTraders)
|
||||
return getValues<User>(topTraders)
|
||||
}
|
||||
|
||||
export function getTopCreators(period: Period) {
|
||||
|
@ -284,7 +274,7 @@ export function getTopCreators(period: Period) {
|
|||
orderBy('creatorVolumeCached.' + period, 'desc'),
|
||||
limit(20)
|
||||
)
|
||||
return getValues(topCreators)
|
||||
return getValues<User>(topCreators)
|
||||
}
|
||||
|
||||
export async function getTopFollowed() {
|
||||
|
|
|
@ -4,6 +4,7 @@ const API_DOCS_URL = 'https://docs.manifold.markets/api'
|
|||
module.exports = {
|
||||
staticPageGenerationTimeout: 600, // e.g. stats page
|
||||
reactStrictMode: true,
|
||||
optimizeFonts: false,
|
||||
experimental: {
|
||||
externalDir: true,
|
||||
optimizeCss: true,
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
"gridjs-react": "5.0.2",
|
||||
"lodash": "4.17.21",
|
||||
"nanoid": "^3.3.4",
|
||||
"next": "12.1.2",
|
||||
"next": "12.2.2",
|
||||
"node-fetch": "3.2.4",
|
||||
"react": "17.0.2",
|
||||
"react-confetti": "6.0.1",
|
||||
|
|
|
@ -6,16 +6,15 @@ export default function Document() {
|
|||
<Html data-theme="mantic" className="min-h-screen">
|
||||
<Head>
|
||||
<link rel="icon" href={ENV_CONFIG.faviconPath} />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin="true"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Readex+Pro:wght@300;400;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
|
@ -24,7 +23,6 @@ export default function Document() {
|
|||
crossOrigin="anonymous"
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<body className="font-readex-pro bg-base-200 min-h-screen">
|
||||
<Main />
|
||||
<NextScript />
|
||||
|
|
|
@ -54,10 +54,8 @@ export default function ContractSearchFirestore(props: {
|
|||
)
|
||||
} else if (sort === 'most-traded') {
|
||||
matches.sort((a, b) => b.volume - a.volume)
|
||||
} else if (sort === 'most-popular') {
|
||||
matches.sort(
|
||||
(a, b) => (b.uniqueBettorCount ?? 0) - (a.uniqueBettorCount ?? 0)
|
||||
)
|
||||
} else if (sort === 'score') {
|
||||
matches.sort((a, b) => (b.popularityScore ?? 0) - (a.popularityScore ?? 0))
|
||||
} else if (sort === '24-hour-vol') {
|
||||
// Use lodash for stable sort, so previous sort breaks all ties.
|
||||
matches = sortBy(matches, ({ volume7Days }) => -1 * volume7Days)
|
||||
|
@ -104,7 +102,7 @@ export default function ContractSearchFirestore(props: {
|
|||
>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="oldest">Oldest</option>
|
||||
<option value="most-popular">Most popular</option>
|
||||
<option value="score">Most popular</option>
|
||||
<option value="most-traded">Most traded</option>
|
||||
<option value="24-hour-vol">24h volume</option>
|
||||
<option value="close-date">Closing soon</option>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { take, sortBy, debounce } from 'lodash'
|
||||
import PlusSmIcon from '@heroicons/react/solid/PlusSmIcon'
|
||||
|
||||
import { Group, GROUP_CHAT_SLUG } from 'common/group'
|
||||
import { Page } from 'web/components/page'
|
||||
|
@ -32,10 +33,7 @@ import { SEO } from 'web/components/SEO'
|
|||
import { Linkify } from 'web/components/linkify'
|
||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||
import { Tabs } from 'web/components/layout/tabs'
|
||||
import {
|
||||
createButtonStyle,
|
||||
CreateQuestionButton,
|
||||
} from 'web/components/create-question-button'
|
||||
import { CreateQuestionButton } from 'web/components/create-question-button'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { GroupChat } from 'web/components/groups/group-chat'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
|
@ -44,7 +42,6 @@ import { getSavedSort } from 'web/hooks/use-sort-and-query-params'
|
|||
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useCommentsOnGroup } from 'web/hooks/use-comments'
|
||||
import { ShareIconButton } from 'web/components/share-icon-button'
|
||||
import { REFERRAL_AMOUNT } from 'common/user'
|
||||
import { ContractSearch } from 'web/components/contract-search'
|
||||
import clsx from 'clsx'
|
||||
|
@ -54,6 +51,7 @@ import { useTipTxns } from 'web/hooks/use-tip-txns'
|
|||
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
|
||||
import { searchInAny } from 'common/util/parse'
|
||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||
import { CopyLinkButton } from 'web/components/copy-link-button'
|
||||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
|
@ -116,7 +114,7 @@ const groupSubpages = [
|
|||
undefined,
|
||||
GROUP_CHAT_SLUG,
|
||||
'questions',
|
||||
'rankings',
|
||||
'leaderboards',
|
||||
'about',
|
||||
] as const
|
||||
|
||||
|
@ -239,9 +237,9 @@ export default function GroupPage(props: {
|
|||
href: groupPath(group.slug, 'questions'),
|
||||
},
|
||||
{
|
||||
title: 'Rankings',
|
||||
title: 'Leaderboards',
|
||||
content: leaderboard,
|
||||
href: groupPath(group.slug, 'rankings'),
|
||||
href: groupPath(group.slug, 'leaderboards'),
|
||||
},
|
||||
{
|
||||
title: 'About',
|
||||
|
@ -255,7 +253,7 @@ export default function GroupPage(props: {
|
|||
<Page
|
||||
rightSidebar={showChatSidebar ? chatTab : undefined}
|
||||
rightSidebarClassName={showChatSidebar ? '!top-0' : ''}
|
||||
className={showChatSidebar ? '!max-w-none !pb-0' : ''}
|
||||
className={showChatSidebar ? '!max-w-7xl !pb-0' : ''}
|
||||
>
|
||||
<SEO
|
||||
title={group.name}
|
||||
|
@ -266,9 +264,7 @@ export default function GroupPage(props: {
|
|||
<Row className={'items-center justify-between gap-4'}>
|
||||
<div className={'sm:mb-1'}>
|
||||
<div
|
||||
className={
|
||||
'line-clamp-1 my-1 text-lg text-indigo-700 sm:my-3 sm:text-2xl'
|
||||
}
|
||||
className={'line-clamp-1 my-2 text-2xl text-indigo-700 sm:my-3'}
|
||||
>
|
||||
{group.name}
|
||||
</div>
|
||||
|
@ -276,7 +272,7 @@ export default function GroupPage(props: {
|
|||
<Linkify text={group.about} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden sm:block xl:hidden">
|
||||
<div className="mt-2">
|
||||
<JoinOrAddQuestionsButtons
|
||||
group={group}
|
||||
user={user}
|
||||
|
@ -284,13 +280,6 @@ export default function GroupPage(props: {
|
|||
/>
|
||||
</div>
|
||||
</Row>
|
||||
<div className="block sm:hidden">
|
||||
<JoinOrAddQuestionsButtons
|
||||
group={group}
|
||||
user={user}
|
||||
isMember={!!isMember}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Tabs
|
||||
currentPageForAnalytics={groupPath(group.slug)}
|
||||
|
@ -309,21 +298,7 @@ function JoinOrAddQuestionsButtons(props: {
|
|||
}) {
|
||||
const { group, user, isMember } = props
|
||||
return user && isMember ? (
|
||||
<Row
|
||||
className={'-mt-2 justify-between sm:mt-0 sm:flex-col sm:justify-center'}
|
||||
>
|
||||
<CreateQuestionButton
|
||||
user={user}
|
||||
overrideText={'Add a new question'}
|
||||
className={'hidden w-48 flex-shrink-0 sm:block'}
|
||||
query={`?groupId=${group.id}`}
|
||||
/>
|
||||
<CreateQuestionButton
|
||||
user={user}
|
||||
overrideText={'New question'}
|
||||
className={'block w-40 flex-shrink-0 sm:hidden'}
|
||||
query={`?groupId=${group.id}`}
|
||||
/>
|
||||
<Row className={'mt-0 justify-end'}>
|
||||
<AddContractButton group={group} user={user} />
|
||||
</Row>
|
||||
) : group.anyoneCanJoin ? (
|
||||
|
@ -354,6 +329,11 @@ function GroupOverview(props: {
|
|||
})
|
||||
}
|
||||
|
||||
const postFix = user ? '?referrer=' + user.username : ''
|
||||
const shareUrl = `https://${ENV_CONFIG.domain}${groupPath(
|
||||
group.slug
|
||||
)}${postFix}`
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col className="gap-2 rounded-b bg-white p-2">
|
||||
|
@ -398,22 +378,25 @@ function GroupOverview(props: {
|
|||
</span>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
{anyoneCanJoin && user && (
|
||||
<Row className={'flex-wrap items-center gap-1'}>
|
||||
<span className={'text-gray-500'}>Share</span>
|
||||
<ShareIconButton
|
||||
copyPayload={`https://${ENV_CONFIG.domain}${groupPath(
|
||||
group.slug
|
||||
)}${user?.username ? '?referrer=' + user?.username : ''}`}
|
||||
buttonClassName={'hover:bg-gray-300 mt-1 !text-gray-700'}
|
||||
>
|
||||
<span className={'mx-2'}>
|
||||
Invite a friend and get M${REFERRAL_AMOUNT} if they sign up!
|
||||
</span>
|
||||
</ShareIconButton>
|
||||
</Row>
|
||||
<Col className="my-4 px-2">
|
||||
<div className="text-lg">Invite</div>
|
||||
<div className={'mb-2 text-gray-500'}>
|
||||
Invite a friend to this group and get M${REFERRAL_AMOUNT} if they
|
||||
sign up!
|
||||
</div>
|
||||
|
||||
<CopyLinkButton
|
||||
url={shareUrl}
|
||||
tracking="copy group share link"
|
||||
buttonClassName="btn-md rounded-l-none"
|
||||
toastClassName={'-left-28 mt-1'}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Col className={'mt-2'}>
|
||||
<div className="mb-2 text-lg">Members</div>
|
||||
<GroupMemberSearch members={members} group={group} />
|
||||
</Col>
|
||||
</Col>
|
||||
|
@ -514,14 +497,14 @@ function GroupLeaderboards(props: {
|
|||
<SortedLeaderboard
|
||||
users={members}
|
||||
scoreFunction={(user) => traderScores[user.id] ?? 0}
|
||||
title="🏅 Bettor rankings"
|
||||
title="🏅 Top bettors"
|
||||
header="Profit"
|
||||
maxToShow={maxToShow}
|
||||
/>
|
||||
<SortedLeaderboard
|
||||
users={members}
|
||||
scoreFunction={(user) => creatorScores[user.id] ?? 0}
|
||||
title="🏅 Creator rankings"
|
||||
title="🏅 Top creators"
|
||||
header="Market volume"
|
||||
maxToShow={maxToShow}
|
||||
/>
|
||||
|
@ -561,7 +544,7 @@ function GroupLeaderboards(props: {
|
|||
}
|
||||
|
||||
function AddContractButton(props: { group: Group; user: User }) {
|
||||
const { group } = props
|
||||
const { group, user } = props
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
async function addContractToCurrentGroup(contract: Contract) {
|
||||
|
@ -571,16 +554,39 @@ function AddContractButton(props: { group: Group; user: User }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex justify-center'}>
|
||||
<button
|
||||
className={clsx('btn btn-sm btn-outline')}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<PlusSmIcon className="h-6 w-6" aria-hidden="true" /> question
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Modal open={open} setOpen={setOpen} className={'sm:p-0'}>
|
||||
<Col
|
||||
className={
|
||||
'max-h-[60vh] min-h-[60vh] w-full gap-4 rounded-md bg-white p-8'
|
||||
'max-h-[60vh] min-h-[60vh] w-full gap-4 rounded-md bg-white'
|
||||
}
|
||||
>
|
||||
<div className={'text-lg text-indigo-700'}>
|
||||
<Col className="p-8 pb-0">
|
||||
<div className={'text-xl text-indigo-700'}>
|
||||
Add a question to your group
|
||||
</div>
|
||||
<div className={'overflow-y-scroll p-1'}>
|
||||
|
||||
<Col className="items-center">
|
||||
<CreateQuestionButton
|
||||
user={user}
|
||||
overrideText={'New question'}
|
||||
className={'w-48 flex-shrink-0 '}
|
||||
query={`?groupId=${group.id}`}
|
||||
/>
|
||||
|
||||
<div className={'mt-2 text-lg text-indigo-700'}>or</div>
|
||||
</Col>
|
||||
</Col>
|
||||
|
||||
<div className={'overflow-y-scroll sm:px-8'}>
|
||||
<ContractSearch
|
||||
hideOrderSelector={true}
|
||||
onContractClick={addContractToCurrentGroup}
|
||||
|
@ -592,26 +598,6 @@ function AddContractButton(props: { group: Group; user: User }) {
|
|||
</div>
|
||||
</Col>
|
||||
</Modal>
|
||||
<div className={'flex justify-center'}>
|
||||
<button
|
||||
className={clsx(
|
||||
createButtonStyle,
|
||||
'hidden w-48 whitespace-nowrap border border-black text-black hover:bg-black hover:text-white sm:block'
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Add an old question
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
createButtonStyle,
|
||||
'block w-40 whitespace-nowrap border border-black text-black hover:bg-black hover:text-white sm:hidden'
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Old question
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { PlusSmIcon } from '@heroicons/react/solid'
|
|||
import { Page } from 'web/components/page'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { getSavedSort } from 'web/hooks/use-sort-and-query-params'
|
||||
import { ContractSearch } from 'web/components/contract-search'
|
||||
import { ContractSearch, DEFAULT_SORT } from 'web/components/contract-search'
|
||||
import { Contract } from 'common/contract'
|
||||
import { ContractPageContent } from './[username]/[contractSlug]'
|
||||
import { getContractFromSlug } from 'web/lib/firebase/contracts'
|
||||
|
@ -28,7 +28,7 @@ const Home = () => {
|
|||
<ContractSearch
|
||||
querySortOptions={{
|
||||
shouldLoadFromStorage: true,
|
||||
defaultSort: getSavedSort() ?? 'most-popular',
|
||||
defaultSort: getSavedSort() ?? DEFAULT_SORT,
|
||||
}}
|
||||
onContractClick={(c) => {
|
||||
// Show contract without navigating to contract page.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { Contract, getContractsBySlugs } from 'web/lib/firebase/contracts'
|
||||
import { Page } from 'web/components/page'
|
||||
import { LandingPagePanel } from 'web/components/landing-page-panel'
|
||||
|
@ -26,6 +27,17 @@ export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => {
|
|||
|
||||
export default function Home(props: { hotContracts: Contract[] }) {
|
||||
const { hotContracts } = props
|
||||
|
||||
// for now this redirect in the component is how we handle the case where they are
|
||||
// on this page and they log in -- in the future we will make some cleaner way
|
||||
const user = useUser()
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
if (user != null) {
|
||||
router.replace('/home')
|
||||
}
|
||||
}, [router, user])
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="px-4 pt-2 md:mt-0 lg:hidden">
|
||||
|
|
|
@ -9,85 +9,88 @@ import {
|
|||
User,
|
||||
} from 'web/lib/firebase/users'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
import { Title } from 'web/components/title'
|
||||
import { Tabs } from 'web/components/layout/tabs'
|
||||
import { useTracking } from 'web/hooks/use-tracking'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz() {
|
||||
return queryLeaderboardUsers('allTime')
|
||||
}
|
||||
const queryLeaderboardUsers = async (period: Period) => {
|
||||
const [topTraders, topCreators, topFollowed] = await Promise.all([
|
||||
getTopTraders(period).catch(() => {}),
|
||||
getTopCreators(period).catch(() => {}),
|
||||
getTopFollowed().catch(() => {}),
|
||||
])
|
||||
export async function getStaticProps() {
|
||||
const props = await fetchProps()
|
||||
|
||||
return {
|
||||
props: {
|
||||
topTraders,
|
||||
topCreators,
|
||||
topFollowed,
|
||||
},
|
||||
props,
|
||||
revalidate: 60, // regenerate after a minute
|
||||
}
|
||||
}
|
||||
|
||||
export default function Leaderboards(props: {
|
||||
const fetchProps = async () => {
|
||||
const [allTime, monthly, weekly, daily] = await Promise.all([
|
||||
queryLeaderboardUsers('allTime'),
|
||||
queryLeaderboardUsers('monthly'),
|
||||
queryLeaderboardUsers('weekly'),
|
||||
queryLeaderboardUsers('daily'),
|
||||
])
|
||||
const topFollowed = await getTopFollowed()
|
||||
|
||||
return {
|
||||
allTime,
|
||||
monthly,
|
||||
weekly,
|
||||
daily,
|
||||
topFollowed,
|
||||
}
|
||||
}
|
||||
|
||||
const queryLeaderboardUsers = async (period: Period) => {
|
||||
const [topTraders, topCreators] = await Promise.all([
|
||||
getTopTraders(period),
|
||||
getTopCreators(period),
|
||||
])
|
||||
return {
|
||||
topTraders,
|
||||
topCreators,
|
||||
}
|
||||
}
|
||||
|
||||
type leaderboard = {
|
||||
topTraders: User[]
|
||||
topCreators: User[]
|
||||
}
|
||||
|
||||
export default function Leaderboards(_props: {
|
||||
allTime: leaderboard
|
||||
monthly: leaderboard
|
||||
weekly: leaderboard
|
||||
daily: leaderboard
|
||||
topFollowed: User[]
|
||||
}) {
|
||||
props = usePropz(props, getStaticPropz) ?? {
|
||||
topTraders: [],
|
||||
topCreators: [],
|
||||
topFollowed: [],
|
||||
}
|
||||
const { topFollowed } = props
|
||||
const [topTradersState, setTopTraders] = useState(props.topTraders)
|
||||
const [topCreatorsState, setTopCreators] = useState(props.topCreators)
|
||||
const [isLoading, setLoading] = useState(false)
|
||||
const [period, setPeriod] = useState<Period>('allTime')
|
||||
|
||||
const [props, setProps] = useState<Parameters<typeof Leaderboards>[0]>(_props)
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
queryLeaderboardUsers(period).then((res) => {
|
||||
setTopTraders(res.props.topTraders as User[])
|
||||
setTopCreators(res.props.topCreators as User[])
|
||||
setLoading(false)
|
||||
})
|
||||
}, [period])
|
||||
fetchProps().then((props) => setProps(props))
|
||||
}, [])
|
||||
|
||||
const { topFollowed } = props
|
||||
|
||||
const LeaderboardWithPeriod = (period: Period) => {
|
||||
const { topTraders, topCreators } = props[period]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col className="mx-4 items-center gap-10 lg:flex-row">
|
||||
{!isLoading ? (
|
||||
<>
|
||||
{period === 'allTime' ||
|
||||
period == 'weekly' ||
|
||||
period === 'daily' ? ( //TODO: show other periods once they're available
|
||||
<Leaderboard
|
||||
title="🏅 Top bettors"
|
||||
users={topTradersState}
|
||||
users={topTraders}
|
||||
columns={[
|
||||
{
|
||||
header: 'Total profit',
|
||||
renderCell: (user) =>
|
||||
formatMoney(user.profitCached[period]),
|
||||
renderCell: (user) => formatMoney(user.profitCached[period]),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<Leaderboard
|
||||
title="🏅 Top creators"
|
||||
users={topCreatorsState}
|
||||
users={topCreators}
|
||||
columns={[
|
||||
{
|
||||
header: 'Total bet',
|
||||
|
@ -96,10 +99,6 @@ export default function Leaderboards(props: {
|
|||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<LoadingIndicator spinnerClassName={'border-gray-500'} />
|
||||
)}
|
||||
</Col>
|
||||
{period === 'allTime' ? (
|
||||
<Col className="mx-4 my-10 items-center gap-10 lg:mx-0 lg:w-1/2 lg:flex-row">
|
||||
|
@ -127,20 +126,17 @@ export default function Leaderboards(props: {
|
|||
<Title text={'Leaderboards'} className={'hidden md:block'} />
|
||||
<Tabs
|
||||
currentPageForAnalytics={'leaderboards'}
|
||||
defaultIndex={0}
|
||||
onClick={(title, index) => {
|
||||
const period = ['allTime', 'monthly', 'weekly', 'daily'][index]
|
||||
setPeriod(period as Period)
|
||||
}}
|
||||
defaultIndex={1}
|
||||
tabs={[
|
||||
{
|
||||
title: 'All Time',
|
||||
content: LeaderboardWithPeriod('allTime'),
|
||||
},
|
||||
{
|
||||
title: 'Monthly',
|
||||
content: LeaderboardWithPeriod('monthly'),
|
||||
},
|
||||
// TODO: Enable this near the end of July!
|
||||
// {
|
||||
// title: 'Monthly',
|
||||
// content: LeaderboardWithPeriod('monthly'),
|
||||
// },
|
||||
{
|
||||
title: 'Weekly',
|
||||
content: LeaderboardWithPeriod('weekly'),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Tabs } from 'web/components/layout/tabs'
|
||||
import { usePrivateUser, useUser } from 'web/hooks/use-user'
|
||||
import { usePrivateUser } from 'web/hooks/use-user'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Notification, notification_source_types } from 'common/notification'
|
||||
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
||||
|
@ -12,17 +12,10 @@ import { UserLink } from 'web/components/user-page'
|
|||
import {
|
||||
MANIFOLD_AVATAR_URL,
|
||||
MANIFOLD_USERNAME,
|
||||
notification_subscribe_types,
|
||||
PrivateUser,
|
||||
User,
|
||||
} from 'common/user'
|
||||
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||
import {
|
||||
getUser,
|
||||
listenForPrivateUser,
|
||||
updatePrivateUser,
|
||||
} from 'web/lib/firebase/users'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
import { getUser } from 'web/lib/firebase/users'
|
||||
import clsx from 'clsx'
|
||||
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
||||
import { Linkify } from 'web/components/linkify'
|
||||
|
@ -37,8 +30,7 @@ import {
|
|||
NotificationGroup,
|
||||
usePreferredGroupedNotifications,
|
||||
} from 'web/hooks/use-notifications'
|
||||
import { CheckIcon, TrendingUpIcon, XIcon } from '@heroicons/react/outline'
|
||||
import toast from 'react-hot-toast'
|
||||
import { TrendingUpIcon } from '@heroicons/react/outline'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { groupPath } from 'web/lib/firebase/groups'
|
||||
import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
|
||||
|
@ -53,6 +45,7 @@ import {
|
|||
redirectIfLoggedOut,
|
||||
} from 'web/lib/firebase/server-auth'
|
||||
import { SiteLink } from 'web/components/site-link'
|
||||
import { NotificationSettings } from 'web/components/NotificationSettings'
|
||||
|
||||
export const NOTIFICATIONS_PER_PAGE = 30
|
||||
const MULTIPLE_USERS_KEY = 'multipleUsers'
|
||||
|
@ -100,15 +93,12 @@ export default function Notifications(props: { user: User }) {
|
|||
privateUser={privateUser}
|
||||
cachedNotifications={localNotifications}
|
||||
/>
|
||||
) : localNotificationGroups &&
|
||||
localNotificationGroups.length > 0 ? (
|
||||
) : (
|
||||
<div className={'min-h-[100vh]'}>
|
||||
<RenderNotificationGroups
|
||||
notificationGroups={localNotificationGroups}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<LoadingIndicator />
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -986,203 +976,3 @@ function getReasonForShowingNotification(
|
|||
}
|
||||
return reasonText
|
||||
}
|
||||
|
||||
// TODO: where should we put referral bonus notifications?
|
||||
function NotificationSettings() {
|
||||
const user = useUser()
|
||||
const [notificationSettings, setNotificationSettings] =
|
||||
useState<notification_subscribe_types>('all')
|
||||
const [emailNotificationSettings, setEmailNotificationSettings] =
|
||||
useState<notification_subscribe_types>('all')
|
||||
const [privateUser, setPrivateUser] = useState<PrivateUser | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (user) listenForPrivateUser(user.id, setPrivateUser)
|
||||
}, [user])
|
||||
|
||||
useEffect(() => {
|
||||
if (!privateUser) return
|
||||
if (privateUser.notificationPreferences) {
|
||||
setNotificationSettings(privateUser.notificationPreferences)
|
||||
}
|
||||
if (
|
||||
privateUser.unsubscribedFromResolutionEmails &&
|
||||
privateUser.unsubscribedFromCommentEmails &&
|
||||
privateUser.unsubscribedFromAnswerEmails
|
||||
) {
|
||||
setEmailNotificationSettings('none')
|
||||
} else if (
|
||||
!privateUser.unsubscribedFromResolutionEmails &&
|
||||
!privateUser.unsubscribedFromCommentEmails &&
|
||||
!privateUser.unsubscribedFromAnswerEmails
|
||||
) {
|
||||
setEmailNotificationSettings('all')
|
||||
} else {
|
||||
setEmailNotificationSettings('less')
|
||||
}
|
||||
}, [privateUser])
|
||||
|
||||
const loading = 'Changing Notifications Settings'
|
||||
const success = 'Notification Settings Changed!'
|
||||
function changeEmailNotifications(newValue: notification_subscribe_types) {
|
||||
if (!privateUser) return
|
||||
if (newValue === 'all') {
|
||||
toast.promise(
|
||||
updatePrivateUser(privateUser.id, {
|
||||
unsubscribedFromResolutionEmails: false,
|
||||
unsubscribedFromCommentEmails: false,
|
||||
unsubscribedFromAnswerEmails: false,
|
||||
}),
|
||||
{
|
||||
loading,
|
||||
success,
|
||||
error: (err) => `${err.message}`,
|
||||
}
|
||||
)
|
||||
} else if (newValue === 'less') {
|
||||
toast.promise(
|
||||
updatePrivateUser(privateUser.id, {
|
||||
unsubscribedFromResolutionEmails: false,
|
||||
unsubscribedFromCommentEmails: true,
|
||||
unsubscribedFromAnswerEmails: true,
|
||||
}),
|
||||
{
|
||||
loading,
|
||||
success,
|
||||
error: (err) => `${err.message}`,
|
||||
}
|
||||
)
|
||||
} else if (newValue === 'none') {
|
||||
toast.promise(
|
||||
updatePrivateUser(privateUser.id, {
|
||||
unsubscribedFromResolutionEmails: true,
|
||||
unsubscribedFromCommentEmails: true,
|
||||
unsubscribedFromAnswerEmails: true,
|
||||
}),
|
||||
{
|
||||
loading,
|
||||
success,
|
||||
error: (err) => `${err.message}`,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function changeInAppNotificationSettings(
|
||||
newValue: notification_subscribe_types
|
||||
) {
|
||||
if (!privateUser) return
|
||||
track('In-App Notification Preferences Changed', {
|
||||
newPreference: newValue,
|
||||
oldPreference: privateUser.notificationPreferences,
|
||||
})
|
||||
toast.promise(
|
||||
updatePrivateUser(privateUser.id, {
|
||||
notificationPreferences: newValue,
|
||||
}),
|
||||
{
|
||||
loading,
|
||||
success,
|
||||
error: (err) => `${err.message}`,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (privateUser && privateUser.notificationPreferences)
|
||||
setNotificationSettings(privateUser.notificationPreferences)
|
||||
else setNotificationSettings('all')
|
||||
}, [privateUser])
|
||||
|
||||
if (!privateUser) {
|
||||
return <LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} />
|
||||
}
|
||||
|
||||
function NotificationSettingLine(props: {
|
||||
label: string
|
||||
highlight: boolean
|
||||
}) {
|
||||
const { label, highlight } = props
|
||||
return (
|
||||
<Row className={clsx('my-1 text-gray-300', highlight && '!text-black')}>
|
||||
{highlight ? <CheckIcon height={20} /> : <XIcon height={20} />}
|
||||
{label}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'p-2'}>
|
||||
<div>In App Notifications</div>
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={notificationSettings}
|
||||
choicesMap={{ All: 'all', Less: 'less', None: 'none' }}
|
||||
setChoice={(choice) =>
|
||||
changeInAppNotificationSettings(
|
||||
choice as notification_subscribe_types
|
||||
)
|
||||
}
|
||||
className={'col-span-4 p-2'}
|
||||
toggleClassName={'w-24'}
|
||||
/>
|
||||
<div className={'mt-4 text-sm'}>
|
||||
<div>
|
||||
<div className={''}>
|
||||
You will receive notifications for:
|
||||
<NotificationSettingLine
|
||||
label={"Resolution of questions you've interacted with"}
|
||||
highlight={notificationSettings !== 'none'}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
highlight={notificationSettings !== 'none'}
|
||||
label={'Activity on your own questions, comments, & answers'}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
highlight={notificationSettings !== 'none'}
|
||||
label={"Activity on questions you're betting on"}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
highlight={notificationSettings !== 'none'}
|
||||
label={"Income & referral bonuses you've received"}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
label={"Activity on questions you've ever bet or commented on"}
|
||||
highlight={notificationSettings === 'all'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'mt-4'}>Email Notifications</div>
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={emailNotificationSettings}
|
||||
choicesMap={{ All: 'all', Less: 'less', None: 'none' }}
|
||||
setChoice={(choice) =>
|
||||
changeEmailNotifications(choice as notification_subscribe_types)
|
||||
}
|
||||
className={'col-span-4 p-2'}
|
||||
toggleClassName={'w-24'}
|
||||
/>
|
||||
<div className={'mt-4 text-sm'}>
|
||||
<div>
|
||||
You will receive emails for:
|
||||
<NotificationSettingLine
|
||||
label={"Resolution of questions you're betting on"}
|
||||
highlight={emailNotificationSettings !== 'none'}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
label={'Closure of your questions'}
|
||||
highlight={emailNotificationSettings !== 'none'}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
label={'Activity on your questions'}
|
||||
highlight={emailNotificationSettings === 'all'}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
label={"Activity on questions you've answered or commented on"}
|
||||
highlight={emailNotificationSettings === 'all'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
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="http://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
|
@ -1,4 +1,5 @@
|
|||
const defaultTheme = require('tailwindcss/defaultTheme')
|
||||
const plugin = require('tailwindcss/plugin')
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
|
@ -32,6 +33,22 @@ module.exports = {
|
|||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/line-clamp'),
|
||||
require('daisyui'),
|
||||
plugin(function ({ addUtilities }) {
|
||||
addUtilities({
|
||||
'.scrollbar-hide': {
|
||||
/* IE and Edge */
|
||||
'-ms-overflow-style': 'none',
|
||||
|
||||
/* Firefox */
|
||||
'scrollbar-width': 'none',
|
||||
|
||||
/* Safari and Chrome */
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
],
|
||||
daisyui: {
|
||||
themes: [
|
||||
|
|
178
yarn.lock
178
yarn.lock
|
@ -2385,10 +2385,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b"
|
||||
integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==
|
||||
|
||||
"@next/env@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.2.tgz#4b0f5fd448ac60b821d2486d2987948e3a099f03"
|
||||
integrity sha512-A/P4ysmFScBFyu1ZV0Mr1Y89snyQhqGwsCrkEpK+itMF+y+pMqBoPVIyakUf4LXqGWJGiGFuIerihvSG70Ad8Q==
|
||||
"@next/env@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.2.tgz#cc1a0a445bd254499e30f632968c03192455f4cc"
|
||||
integrity sha512-BqDwE4gDl1F608TpnNxZqrCn6g48MBjvmWFEmeX5wEXDXh3IkAOw6ASKUgjT8H4OUePYFqghDFUss5ZhnbOUjw==
|
||||
|
||||
"@next/eslint-plugin-next@12.1.6":
|
||||
version "12.1.6"
|
||||
|
@ -2397,65 +2397,70 @@
|
|||
dependencies:
|
||||
glob "7.1.7"
|
||||
|
||||
"@next/swc-android-arm-eabi@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.2.tgz#675e952d9032ac7bec02f3f413c17d33bbd90857"
|
||||
integrity sha512-iwalfLBhYmCIlj09czFbovj1SmTycf0AGR8CB357wgmEN8xIuznIwSsCH87AhwQ9apfNtdeDhxvuKmhS9T3FqQ==
|
||||
"@next/swc-android-arm-eabi@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.2.tgz#f6c4111e6371f73af6bf80c9accb3d96850a92cd"
|
||||
integrity sha512-VHjuCHeq9qCprUZbsRxxM/VqSW8MmsUtqB5nEpGEgUNnQi/BTm/2aK8tl7R4D0twGKRh6g1AAeFuWtXzk9Z/vQ==
|
||||
|
||||
"@next/swc-android-arm64@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.2.tgz#d9710c50853235f258726b19a649df9c29a49682"
|
||||
integrity sha512-ZoR0Vx7czJhTgRAcFbzTKQc2n2ChC036/uc6PbgYiI/LreEnfmsV/CiREP0pUVs5ndntOX8kBA3BSbh4zCO5tQ==
|
||||
"@next/swc-android-arm64@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.2.tgz#b69de59c51e631a7600439e7a8993d6e82f3369e"
|
||||
integrity sha512-v5EYzXUOSv0r9mO/2PX6mOcF53k8ndlu9yeFHVAWW1Dhw2jaJcvTRcCAwYYN8Q3tDg0nH3NbEltJDLKmcJOuVA==
|
||||
|
||||
"@next/swc-darwin-arm64@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.2.tgz#aadd21b711c82b3efa9b4ecf7665841259e1fa7e"
|
||||
integrity sha512-VXv7lpqFjHwkK65CZHkjvBxlSBTG+l3O0Zl2zHniHj0xHzxJZvR8VFjV2zIMZCYSfVqeQ5yt2rjwuQ9zbpGtXQ==
|
||||
"@next/swc-darwin-arm64@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.2.tgz#80157c91668eff95b72d052428c353eab0fc4c50"
|
||||
integrity sha512-JCoGySHKGt+YBk7xRTFGx1QjrnCcwYxIo3yGepcOq64MoiocTM3yllQWeOAJU2/k9MH0+B5E9WUSme4rOCBbpA==
|
||||
|
||||
"@next/swc-darwin-x64@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.2.tgz#3b1a389828f5c88ecb828a6394692fdeaf175081"
|
||||
integrity sha512-evXxJQnXEnU+heWyun7d0UV6bhBcmoiyFGR3O3v9qdhGbeXh+SXYVxRO69juuh6V7RWRdlb1KQ0rGUNa1k0XSw==
|
||||
"@next/swc-darwin-x64@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.2.tgz#12be2f58e676fccff3d48a62921b9927ed295133"
|
||||
integrity sha512-dztDtvfkhUqiqpXvrWVccfGhLe44yQ5tQ7B4tBfnsOR6vxzI9DNPHTlEOgRN9qDqTAcFyPxvg86mn4l8bB9Jcw==
|
||||
|
||||
"@next/swc-linux-arm-gnueabihf@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.2.tgz#db4371ca716bf94c94d4f6b001ac3c9d08d97d79"
|
||||
integrity sha512-LJV/wo6R0Ot7Y/20bZs00aBG4J333RT6H/5Q2AROE4Hnx7cenSktSnfU6WCnJgzYLSIHdbLs549LcZMULuVquw==
|
||||
"@next/swc-freebsd-x64@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.2.tgz#de1363431a49059f1efb8c0f86ce6a79c53b3a95"
|
||||
integrity sha512-JUnXB+2xfxqsAvhFLPJpU1NeyDsvJrKoOjpV7g3Dxbno2Riu4tDKn3kKF886yleAuD/1qNTUCpqubTvbbT2VoA==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.2.tgz#0e71db03b8b12ed315c8be7d15392ecefe562b7c"
|
||||
integrity sha512-fjlYU1Y8kVjjRKyuyQBYLHPxjGOS2ox7U8TqAvtgKvd2PxqdsgW4sP+VDovRVPrZlGXNllKoJiqMO1OoR9fB6w==
|
||||
"@next/swc-linux-arm-gnueabihf@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.2.tgz#d5b8e0d1bb55bbd9db4d2fec018217471dc8b9e6"
|
||||
integrity sha512-XeYC/qqPLz58R4pjkb+x8sUUxuGLnx9QruC7/IGkK68yW4G17PHwKI/1njFYVfXTXUukpWjcfBuauWwxp9ke7Q==
|
||||
|
||||
"@next/swc-linux-arm64-musl@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.2.tgz#f1b055793da1c12167ed3b6e32aef8289721a1fb"
|
||||
integrity sha512-Y1JRDMHqSjLObjyrD1hf6ePrJcOF/mkw+LbAzoNgrHL1dSuIAqcz3jYunJt8T7Yw48xSJy6LPSL9BclAHwEwOA==
|
||||
"@next/swc-linux-arm64-gnu@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.2.tgz#3bc75984e1d5ec8f59eb53702cc382d8e1be2061"
|
||||
integrity sha512-d6jT8xgfKYFkzR7J0OHo2D+kFvY/6W8qEo6/hmdrTt6AKAqxs//rbbcdoyn3YQq1x6FVUUd39zzpezZntg9Naw==
|
||||
|
||||
"@next/swc-linux-x64-gnu@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.2.tgz#69764ffaacb3b9b373897fff15d7dd871455efe2"
|
||||
integrity sha512-5N4QSRT60ikQqCU8iHfYZzlhg6MFTLsKhMTARmhn8wLtZfN9VVyTFwZrJQWjV64dZc4JFeXDANGao8fm55y6bw==
|
||||
"@next/swc-linux-arm64-musl@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.2.tgz#270db73e07a18d999f61e79a917943fa5bc1ef56"
|
||||
integrity sha512-rIZRFxI9N/502auJT1i7coas0HTHUM+HaXMyJiCpnY8Rimbo0495ir24tzzHo3nQqJwcflcPTwEh/DV17sdv9A==
|
||||
|
||||
"@next/swc-linux-x64-musl@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.2.tgz#0ddaedb5ec578c01771f83be2046dafb2f70df91"
|
||||
integrity sha512-b32F/xAgdYG4Pt0foFzhF+2uhvNxnEj7aJNp1R4EhZotdej2PzvFWcP/dGkc7MJl205pBz5oC3gHyILIIlW6XA==
|
||||
"@next/swc-linux-x64-gnu@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.2.tgz#e6c72fa20478552e898c434f4d4c0c5e89d2ea78"
|
||||
integrity sha512-ir1vNadlUDj7eQk15AvfhG5BjVizuCHks9uZwBfUgT5jyeDCeRvaDCo1+Q6+0CLOAnYDR/nqSCvBgzG2UdFh9A==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.2.tgz#9e17ed56d5621f8c6961193da3a0b155cea511c9"
|
||||
integrity sha512-hVOcGmWDeVwO00Aclopsj6MoYhfJl5zA4vjAai9KjgclQTFZa/DC0vQjgKAHHKGT5oMHgjiq/G7L6P1/UfwYnw==
|
||||
"@next/swc-linux-x64-musl@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.2.tgz#b9ef9efe2c401839cdefa5e70402386aafdce15a"
|
||||
integrity sha512-bte5n2GzLN3O8JdSFYWZzMgEgDHZmRz5wiispiiDssj4ik3l8E7wq/czNi8RmIF+ioj2sYVokUNa/ekLzrESWw==
|
||||
|
||||
"@next/swc-win32-ia32-msvc@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.2.tgz#ddd260cbe8bc4002fb54415b80baccf37f8db783"
|
||||
integrity sha512-wnVDGIVz2pR3vIkyN6IE+1NvMSBrBj1jba11iR16m8TAPzZH/PrNsxr0a9N5VavEXXLcQpoUVvT+N7nflbRAHg==
|
||||
"@next/swc-win32-arm64-msvc@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.2.tgz#18fa7ec7248da3a7926a0601d9ececc53ac83157"
|
||||
integrity sha512-ZUGCmcDmdPVSAlwJ/aD+1F9lYW8vttseiv4n2+VCDv5JloxiX9aY32kYZaJJO7hmTLNrprvXkb4OvNuHdN22Jg==
|
||||
|
||||
"@next/swc-win32-x64-msvc@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.2.tgz#37412a314bcf4c6006a74e1ef9764048344f3848"
|
||||
integrity sha512-MLNcurEpQp0+7OU9261f7PkN52xTGkfrt4IYTIXau7DO/aHj927oK6piIJdl9EOHdX/KN5W6qlyErj170PSHtw==
|
||||
"@next/swc-win32-ia32-msvc@12.2.2":
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.2.tgz#54936e84f4a219441d051940354da7cd3eafbb4f"
|
||||
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":
|
||||
version "0.74.0"
|
||||
|
@ -2837,6 +2842,13 @@
|
|||
"@svgr/plugin-jsx" "^6.2.1"
|
||||
"@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":
|
||||
version "1.1.2"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz#59590c8ffa8b5939cf4161f00827b8873ad72498"
|
||||
integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA==
|
||||
|
@ -8320,29 +8332,31 @@ next-sitemap@^2.5.14:
|
|||
"@corex/deepmerge" "^2.6.148"
|
||||
minimist "^1.2.6"
|
||||
|
||||
next@12.1.2:
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-12.1.2.tgz#c5376a8ae17d3e404a2b691c01f94c8943306f29"
|
||||
integrity sha512-JHPCsnFTBO0Z4SQxSYc611UA1WA+r/3y3Neg66AH5/gSO/oksfRnFw/zGX/FZ9+oOUHS9y3wJFawNpVYR2gJSQ==
|
||||
next@12.2.2:
|
||||
version "12.2.2"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-12.2.2.tgz#029bf5e4a18a891ca5d05b189b7cd983fd22c072"
|
||||
integrity sha512-zAYFY45aBry/PlKONqtlloRFqU/We3zWYdn2NoGvDZkoYUYQSJC8WMcalS5C19MxbCZLUVCX7D7a6gTGgl2yLg==
|
||||
dependencies:
|
||||
"@next/env" "12.1.2"
|
||||
caniuse-lite "^1.0.30001283"
|
||||
"@next/env" "12.2.2"
|
||||
"@swc/helpers" "0.4.2"
|
||||
caniuse-lite "^1.0.30001332"
|
||||
postcss "8.4.5"
|
||||
styled-jsx "5.0.1"
|
||||
use-subscription "1.5.1"
|
||||
styled-jsx "5.0.2"
|
||||
use-sync-external-store "1.1.0"
|
||||
optionalDependencies:
|
||||
"@next/swc-android-arm-eabi" "12.1.2"
|
||||
"@next/swc-android-arm64" "12.1.2"
|
||||
"@next/swc-darwin-arm64" "12.1.2"
|
||||
"@next/swc-darwin-x64" "12.1.2"
|
||||
"@next/swc-linux-arm-gnueabihf" "12.1.2"
|
||||
"@next/swc-linux-arm64-gnu" "12.1.2"
|
||||
"@next/swc-linux-arm64-musl" "12.1.2"
|
||||
"@next/swc-linux-x64-gnu" "12.1.2"
|
||||
"@next/swc-linux-x64-musl" "12.1.2"
|
||||
"@next/swc-win32-arm64-msvc" "12.1.2"
|
||||
"@next/swc-win32-ia32-msvc" "12.1.2"
|
||||
"@next/swc-win32-x64-msvc" "12.1.2"
|
||||
"@next/swc-android-arm-eabi" "12.2.2"
|
||||
"@next/swc-android-arm64" "12.2.2"
|
||||
"@next/swc-darwin-arm64" "12.2.2"
|
||||
"@next/swc-darwin-x64" "12.2.2"
|
||||
"@next/swc-freebsd-x64" "12.2.2"
|
||||
"@next/swc-linux-arm-gnueabihf" "12.2.2"
|
||||
"@next/swc-linux-arm64-gnu" "12.2.2"
|
||||
"@next/swc-linux-arm64-musl" "12.2.2"
|
||||
"@next/swc-linux-x64-gnu" "12.2.2"
|
||||
"@next/swc-linux-x64-musl" "12.2.2"
|
||||
"@next/swc-win32-arm64-msvc" "12.2.2"
|
||||
"@next/swc-win32-ia32-msvc" "12.2.2"
|
||||
"@next/swc-win32-x64-msvc" "12.2.2"
|
||||
|
||||
no-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
|
@ -10892,10 +10906,10 @@ style-to-object@0.3.0, style-to-object@^0.3.0:
|
|||
dependencies:
|
||||
inline-style-parser "0.1.1"
|
||||
|
||||
styled-jsx@5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.1.tgz#78fecbbad2bf95ce6cd981a08918ce4696f5fc80"
|
||||
integrity sha512-+PIZ/6Uk40mphiQJJI1202b+/dYeTVd9ZnMPR80pgiWbjIwvN2zIp4r9et0BgqBuShh48I0gttPlAXA7WVvBxw==
|
||||
styled-jsx@5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.2.tgz#ff230fd593b737e9e68b630a694d460425478729"
|
||||
integrity sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ==
|
||||
|
||||
stylehacks@^5.1.0:
|
||||
version "5.1.0"
|
||||
|
@ -11437,12 +11451,10 @@ use-latest@^1.2.1:
|
|||
dependencies:
|
||||
use-isomorphic-layout-effect "^1.1.1"
|
||||
|
||||
use-subscription@1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
|
||||
integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA==
|
||||
dependencies:
|
||||
object-assign "^4.1.1"
|
||||
use-sync-external-store@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82"
|
||||
integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
|
|
Loading…
Reference in New Issue
Block a user