Merge branch 'main' into no-style
This commit is contained in:
commit
3661fa85a5
|
@ -8,11 +8,11 @@
|
|||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@tiptap/core": "2.0.0-beta.181",
|
||||
"@tiptap/core": "2.0.0-beta.182",
|
||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.191",
|
||||
"lodash": "4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -26,11 +26,11 @@
|
|||
"dependencies": {
|
||||
"@amplitude/node": "1.10.0",
|
||||
"@google-cloud/functions-framework": "3.1.2",
|
||||
"@tiptap/core": "2.0.0-beta.181",
|
||||
"@tiptap/core": "2.0.0-beta.182",
|
||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.191",
|
||||
"cors": "2.8.5",
|
||||
"dayjs": "1.11.4",
|
||||
"express": "4.18.1",
|
||||
|
|
|
@ -71,10 +71,11 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
|
|||
const yTickValues = [0, 25, 50, 75, 100]
|
||||
|
||||
const numXTickValues = isLargeWidth ? 5 : 2
|
||||
const hoursAgo = latestTime.subtract(5, 'hours')
|
||||
const startDate = dayjs(contract.createdTime).isBefore(hoursAgo)
|
||||
? new Date(contract.createdTime)
|
||||
: hoursAgo.toDate()
|
||||
const startDate = new Date(contract.createdTime)
|
||||
const endDate = dayjs(startDate).add(1, 'hour').isAfter(latestTime)
|
||||
? latestTime.add(1, 'hours').toDate()
|
||||
: latestTime.toDate()
|
||||
const includeMinute = dayjs(endDate).diff(startDate, 'hours') < 2
|
||||
|
||||
const multiYear = !dayjs(startDate).isSame(latestTime, 'year')
|
||||
const lessThanAWeek = dayjs(startDate).add(1, 'week').isAfter(latestTime)
|
||||
|
@ -96,16 +97,24 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
|
|||
xScale={{
|
||||
type: 'time',
|
||||
min: startDate,
|
||||
max: latestTime.toDate(),
|
||||
max: endDate,
|
||||
}}
|
||||
xFormat={(d) =>
|
||||
formatTime(+d.valueOf(), multiYear, lessThanAWeek, lessThanAWeek)
|
||||
}
|
||||
axisBottom={{
|
||||
tickValues: numXTickValues,
|
||||
format: (time) => formatTime(+time, multiYear, lessThanAWeek, false),
|
||||
format: (time) =>
|
||||
formatTime(+time, multiYear, lessThanAWeek, includeMinute),
|
||||
}}
|
||||
colors={{ scheme: 'pastel1' }}
|
||||
colors={[
|
||||
'#fca5a5', // red-300
|
||||
'#93c5fd', // blue-300
|
||||
'#86efac', // green-300
|
||||
'#f9a8d4', // pink-300
|
||||
'#a5b4fc', // indigo-300
|
||||
'#fcd34d', // amber-300
|
||||
]}
|
||||
pointSize={0}
|
||||
curve="stepAfter"
|
||||
enableSlices="x"
|
||||
|
@ -156,7 +165,11 @@ function formatTime(
|
|||
) {
|
||||
const d = dayjs(time)
|
||||
|
||||
if (d.add(1, 'minute').isAfter(Date.now())) return 'Now'
|
||||
if (
|
||||
d.add(1, 'minute').isAfter(Date.now()) &&
|
||||
d.subtract(1, 'minute').isBefore(Date.now())
|
||||
)
|
||||
return 'Now'
|
||||
|
||||
let format: string
|
||||
if (d.isSame(Date.now(), 'day')) {
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import { SparklesIcon } from '@heroicons/react/solid'
|
||||
|
||||
export function FeaturedContractBadge() {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-green-100 px-3 py-0.5 text-sm font-medium text-blue-800">
|
||||
<SparklesIcon className="h-4 w-4" aria-hidden="true" /> Featured
|
||||
</span>
|
||||
)
|
||||
}
|
|
@ -38,6 +38,7 @@ export function ContractCard(props: {
|
|||
showHotVolume?: boolean
|
||||
showTime?: ShowTime
|
||||
className?: string
|
||||
questionClass?: string
|
||||
onClick?: () => void
|
||||
hideQuickBet?: boolean
|
||||
hideGroupLink?: boolean
|
||||
|
@ -46,6 +47,7 @@ export function ContractCard(props: {
|
|||
showHotVolume,
|
||||
showTime,
|
||||
className,
|
||||
questionClass,
|
||||
onClick,
|
||||
hideQuickBet,
|
||||
hideGroupLink,
|
||||
|
@ -68,7 +70,7 @@ export function ContractCard(props: {
|
|||
return (
|
||||
<Row
|
||||
className={clsx(
|
||||
'relative gap-3 self-start rounded-lg bg-white shadow-md hover:cursor-pointer hover:bg-gray-100',
|
||||
'relative gap-3 rounded-lg bg-white shadow-md hover:cursor-pointer hover:bg-gray-100',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
@ -104,7 +106,12 @@ export function ContractCard(props: {
|
|||
contract={contract}
|
||||
className={'hidden md:inline-flex'}
|
||||
/>
|
||||
<p className="break-anywhere font-semibold text-indigo-700 group-hover:underline group-hover:decoration-indigo-400 group-hover:decoration-2">
|
||||
<p
|
||||
className={clsx(
|
||||
'break-anywhere font-semibold text-indigo-700 group-hover:underline group-hover:decoration-indigo-400 group-hover:decoration-2',
|
||||
questionClass
|
||||
)}
|
||||
>
|
||||
{question}
|
||||
</p>
|
||||
|
||||
|
@ -162,11 +169,7 @@ export function ContractCard(props: {
|
|||
showQuickBet ? 'w-[85%]' : 'w-full'
|
||||
)}
|
||||
>
|
||||
<AvatarDetails
|
||||
contract={contract}
|
||||
short={true}
|
||||
className={'block md:hidden'}
|
||||
/>
|
||||
<AvatarDetails contract={contract} short={true} className="md:hidden" />
|
||||
<MiscDetails
|
||||
contract={contract}
|
||||
showHotVolume={showHotVolume}
|
||||
|
|
|
@ -5,12 +5,15 @@ import {
|
|||
TrendingUpIcon,
|
||||
UserGroupIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import Router from 'next/router'
|
||||
import clsx from 'clsx'
|
||||
import { Editor } from '@tiptap/react'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { Row } from '../layout/row'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { UserLink } from '../user-page'
|
||||
import { Contract, updateContract } from 'web/lib/firebase/contracts'
|
||||
import dayjs from 'dayjs'
|
||||
import { DateTimeTooltip } from '../datetime-tooltip'
|
||||
import { fromNow } from 'web/lib/util/time'
|
||||
import { Avatar } from '../avatar'
|
||||
|
@ -21,7 +24,6 @@ import NewContractBadge from '../new-contract-badge'
|
|||
import { UserFollowButton } from '../follow-button'
|
||||
import { DAY_MS } from 'common/util/time'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { Editor } from '@tiptap/react'
|
||||
import { exhibitExts } from 'common/util/parse'
|
||||
import { Button } from 'web/components/button'
|
||||
import { Modal } from 'web/components/layout/modal'
|
||||
|
@ -30,10 +32,9 @@ import { ContractGroupsList } from 'web/components/groups/contract-groups-list'
|
|||
import { SiteLink } from 'web/components/site-link'
|
||||
import { groupPath } from 'web/lib/firebase/groups'
|
||||
import { insertContent } from '../editor/utils'
|
||||
import clsx from 'clsx'
|
||||
import { contractMetrics } from 'common/contract-details'
|
||||
import { User } from 'common/user'
|
||||
import { FeaturedContractBadge } from 'web/components/contract/FeaturedContractBadge'
|
||||
import { FeaturedContractBadge } from 'web/components/contract/featured-contract-badge'
|
||||
|
||||
export type ShowTime = 'resolve-date' | 'close-date'
|
||||
|
||||
|
@ -191,7 +192,11 @@ export function ContractDetails(props: {
|
|||
size={'xs'}
|
||||
className={'max-w-[200px]'}
|
||||
color={'gray-white'}
|
||||
onClick={() => setOpen(!open)}
|
||||
onClick={() =>
|
||||
groupToDisplay
|
||||
? Router.push(groupPath(groupToDisplay.slug))
|
||||
: setOpen(!open)
|
||||
}
|
||||
>
|
||||
{groupInfo}
|
||||
</Button>
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useAdmin, useDev } from 'web/hooks/use-admin'
|
|||
import { SiteLink } from '../site-link'
|
||||
import { firestoreConsolePath } from 'common/envs/constants'
|
||||
import { deleteField } from 'firebase/firestore'
|
||||
import ShortToggle from '../widgets/short-toggle'
|
||||
|
||||
export const contractDetailsButtonClassName =
|
||||
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
||||
|
@ -50,6 +51,21 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
|||
? 'Multiple choice'
|
||||
: 'Numeric'
|
||||
|
||||
const onFeaturedToggle = async (enabled: boolean) => {
|
||||
if (
|
||||
enabled &&
|
||||
(contract.featuredOnHomeRank === 0 || !contract?.featuredOnHomeRank)
|
||||
) {
|
||||
await updateContract(id, { featuredOnHomeRank: 1 })
|
||||
setFeatured(true)
|
||||
} else if (!enabled && (contract?.featuredOnHomeRank ?? 0) > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
await updateContract(id, { featuredOnHomeRank: deleteField() })
|
||||
setFeatured(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
|
@ -144,43 +160,13 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
|||
)}
|
||||
{isAdmin && (
|
||||
<tr>
|
||||
<td>Set featured</td>
|
||||
<td>[ADMIN] Featured</td>
|
||||
<td>
|
||||
<select
|
||||
className="select select-bordered"
|
||||
value={featured ? 'true' : 'false'}
|
||||
onChange={(e) => {
|
||||
const newVal = e.target.value === 'true'
|
||||
if (
|
||||
newVal &&
|
||||
(contract.featuredOnHomeRank === 0 ||
|
||||
!contract?.featuredOnHomeRank)
|
||||
)
|
||||
updateContract(id, {
|
||||
featuredOnHomeRank: 1,
|
||||
})
|
||||
.then(() => {
|
||||
setFeatured(true)
|
||||
})
|
||||
.catch(console.error)
|
||||
else if (
|
||||
!newVal &&
|
||||
(contract?.featuredOnHomeRank ?? 0) > 0
|
||||
)
|
||||
updateContract(id, {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
featuredOnHomeRank: deleteField(),
|
||||
})
|
||||
.then(() => {
|
||||
setFeatured(false)
|
||||
})
|
||||
.catch(console.error)
|
||||
}}
|
||||
>
|
||||
<option value="false">false</option>
|
||||
<option value="true">true</option>
|
||||
</select>
|
||||
<ShortToggle
|
||||
enabled={featured}
|
||||
setEnabled={setFeatured}
|
||||
onChange={onFeaturedToggle}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
|
|
@ -58,10 +58,11 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
|||
const { width } = useWindowSize()
|
||||
|
||||
const numXTickValues = !width || width < 800 ? 2 : 5
|
||||
const hoursAgo = latestTime.subtract(1, 'hours')
|
||||
const startDate = dayjs(times[0]).isBefore(hoursAgo)
|
||||
? times[0]
|
||||
: hoursAgo.toDate()
|
||||
const startDate = times[0]
|
||||
const endDate = dayjs(startDate).add(1, 'hour').isAfter(latestTime)
|
||||
? latestTime.add(1, 'hours').toDate()
|
||||
: latestTime.toDate()
|
||||
const includeMinute = dayjs(endDate).diff(startDate, 'hours') < 2
|
||||
|
||||
// Minimum number of points for the graph to have. For smooth tooltip movement
|
||||
// On first load, width is undefined, skip adding extra points to let page load faster
|
||||
|
@ -133,14 +134,15 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
|||
xScale={{
|
||||
type: 'time',
|
||||
min: startDate,
|
||||
max: latestTime.toDate(),
|
||||
max: endDate,
|
||||
}}
|
||||
xFormat={(d) =>
|
||||
formatTime(+d.valueOf(), multiYear, lessThanAWeek, lessThanAWeek)
|
||||
}
|
||||
axisBottom={{
|
||||
tickValues: numXTickValues,
|
||||
format: (time) => formatTime(+time, multiYear, lessThanAWeek, false),
|
||||
format: (time) =>
|
||||
formatTime(+time, multiYear, lessThanAWeek, includeMinute),
|
||||
}}
|
||||
colors={{ datum: 'color' }}
|
||||
curve="stepAfter"
|
||||
|
@ -183,7 +185,11 @@ function formatTime(
|
|||
) {
|
||||
const d = dayjs(time)
|
||||
|
||||
if (d.add(1, 'minute').isAfter(Date.now())) return 'Now'
|
||||
if (
|
||||
d.add(1, 'minute').isAfter(Date.now()) &&
|
||||
d.subtract(1, 'minute').isBefore(Date.now())
|
||||
)
|
||||
return 'Now'
|
||||
|
||||
let format: string
|
||||
if (d.isSame(Date.now(), 'day')) {
|
||||
|
|
9
web/components/contract/featured-contract-badge.tsx
Normal file
9
web/components/contract/featured-contract-badge.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { BadgeCheckIcon } from '@heroicons/react/solid'
|
||||
|
||||
export function FeaturedContractBadge() {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-green-100 px-3 py-0.5 text-sm font-medium text-green-800">
|
||||
<BadgeCheckIcon className="h-4 w-4" aria-hidden="true" /> Featured
|
||||
</span>
|
||||
)
|
||||
}
|
|
@ -40,6 +40,11 @@ const embedPatterns: EmbedPattern[] = [
|
|||
rewrite: (id) =>
|
||||
`<iframe src="https://www.metaculus.com/questions/embed/${id}"></iframe>`,
|
||||
},
|
||||
{
|
||||
regex: /^(https?:\/\/www\.figma\.com\/(?:file|proto)\/[^\/]+\/[^\/]+)/,
|
||||
rewrite: (url) =>
|
||||
`<iframe src="https://www.figma.com/embed?embed_host=manifold&url=${url}"></iframe>`,
|
||||
},
|
||||
// Twitch is a bit annoying, since it requires the `&parent=DOMAIN` to match
|
||||
{
|
||||
// Twitch: https://www.twitch.tv/videos/1445087149
|
||||
|
|
|
@ -48,8 +48,17 @@ export function MarketModal(props: {
|
|||
{contracts.length > 1 && 's'}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => setContracts([])} color="gray">
|
||||
Cancel
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (contracts.length > 0) {
|
||||
setContracts([])
|
||||
} else {
|
||||
setOpen(false)
|
||||
}
|
||||
}}
|
||||
color="gray"
|
||||
>
|
||||
{contracts.length > 0 ? 'Reset' : 'Cancel'}
|
||||
</Button>
|
||||
</Row>
|
||||
)}
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
CashIcon,
|
||||
HeartIcon,
|
||||
UserGroupIcon,
|
||||
TrendingUpIcon,
|
||||
ChatIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import clsx from 'clsx'
|
||||
|
@ -28,6 +27,7 @@ import { Group } from 'common/group'
|
|||
import { Spacer } from '../layout/spacer'
|
||||
import { CHALLENGES_ENABLED } from 'common/challenge'
|
||||
import { buildArray } from 'common/util/array'
|
||||
import TrophyIcon from 'web/lib/icons/trophy-icon'
|
||||
|
||||
const logout = async () => {
|
||||
// log out, and then reload the page, in case SSR wants to boot them out
|
||||
|
@ -45,11 +45,12 @@ function getNavigation() {
|
|||
icon: NotificationsIcon,
|
||||
},
|
||||
|
||||
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
||||
|
||||
...(IS_PRIVATE_MANIFOLD
|
||||
? []
|
||||
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
||||
: [
|
||||
{ name: 'Get M$', href: '/add-funds', icon: CashIcon },
|
||||
{ name: 'Tournaments', href: '/tournaments', icon: TrophyIcon },
|
||||
]),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -69,11 +70,9 @@ function getMoreNavigation(user?: User | null) {
|
|||
return buildArray(
|
||||
CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' },
|
||||
[
|
||||
{ name: 'Leaderboards', href: '/leaderboards' },
|
||||
{ name: 'Tournaments', href: '/tournaments' },
|
||||
{ name: 'Charity', href: '/charity' },
|
||||
{
|
||||
name: 'Salem tournament',
|
||||
href: 'https://salemcenter.manifold.markets/',
|
||||
},
|
||||
{ name: 'Blog', href: 'https://news.manifold.markets' },
|
||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
|
||||
|
@ -85,12 +84,9 @@ function getMoreNavigation(user?: User | null) {
|
|||
CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' },
|
||||
[
|
||||
{ name: 'Referrals', href: '/referrals' },
|
||||
{ name: 'Leaderboards', href: '/leaderboards' },
|
||||
{ name: 'Charity', href: '/charity' },
|
||||
{ name: 'Send M$', href: '/links' },
|
||||
{
|
||||
name: 'Salem tournament',
|
||||
href: 'https://salemcenter.manifold.markets/',
|
||||
},
|
||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||
{ name: 'Help & About', href: 'https://help.manifold.markets/' },
|
||||
{
|
||||
|
@ -119,12 +115,12 @@ const signedOutMobileNavigation = [
|
|||
icon: BookOpenIcon,
|
||||
},
|
||||
{ name: 'Charity', href: '/charity', icon: HeartIcon },
|
||||
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
||||
{ name: 'Tournaments', href: '/tournaments', icon: TrophyIcon },
|
||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh', icon: ChatIcon },
|
||||
]
|
||||
|
||||
const signedInMobileNavigation = [
|
||||
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
||||
{ name: 'Tournaments', href: '/tournaments', icon: TrophyIcon },
|
||||
...(IS_PRIVATE_MANIFOLD
|
||||
? []
|
||||
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
||||
|
@ -147,10 +143,7 @@ function getMoreMobileNav() {
|
|||
CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' },
|
||||
[
|
||||
{ name: 'Referrals', href: '/referrals' },
|
||||
{
|
||||
name: 'Salem tournament',
|
||||
href: 'https://salemcenter.manifold.markets/',
|
||||
},
|
||||
{ name: 'Leaderboards', href: '/leaderboards' },
|
||||
{ name: 'Charity', href: '/charity' },
|
||||
{ name: 'Send M$', href: '/links' },
|
||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||
|
|
|
@ -5,13 +5,19 @@ import clsx from 'clsx'
|
|||
export default function ShortToggle(props: {
|
||||
enabled: boolean
|
||||
setEnabled: (enabled: boolean) => void
|
||||
onChange?: (enabled: boolean) => void
|
||||
}) {
|
||||
const { enabled, setEnabled } = props
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={setEnabled}
|
||||
onChange={(e: boolean) => {
|
||||
setEnabled(e)
|
||||
if (props.onChange) {
|
||||
props.onChange(e)
|
||||
}
|
||||
}}
|
||||
className="group relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
|
|
27
web/lib/icons/trophy-icon.tsx
Normal file
27
web/lib/icons/trophy-icon.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
export default function TrophyIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentcolor"
|
||||
stroke-width="2"
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="m6,5c0,4 1.4,7.8 3.5,8.5l0,2c-1.2,0.7 -1.2,1 -1.6,4l8,0c-0.4,-3 -0.4,-3.3 -1.6,-4l0,-2c2.1,-0.7 3.5,-4.5 3.5,-8.5z"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="m6.2,8.3c-2.5,-1.6 -3.5,1 -3,2.5c1,1.7 2.6,2.5 4.5,1.8"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="m17.6,8.3c2.5,-1.6 3.5,1 3,2.5c-1,1.7 -2.6,2.5 -4.5,1.8"
|
||||
fill="none"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
|
@ -27,14 +27,14 @@
|
|||
"@nivo/line": "0.74.0",
|
||||
"@nivo/tooltip": "0.74.0",
|
||||
"@react-query-firebase/firestore": "0.4.2",
|
||||
"@tiptap/core": "2.0.0-beta.181",
|
||||
"@tiptap/core": "2.0.0-beta.182",
|
||||
"@tiptap/extension-character-count": "2.0.0-beta.31",
|
||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||
"@tiptap/extension-placeholder": "2.0.0-beta.53",
|
||||
"@tiptap/react": "2.0.0-beta.114",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.191",
|
||||
"algoliasearch": "4.13.0",
|
||||
"browser-image-compression": "2.0.0",
|
||||
"clsx": "1.1.1",
|
||||
|
|
|
@ -166,7 +166,8 @@ export function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'FREE_RESPONSE' && (
|
||||
{(outcomeType === 'FREE_RESPONSE' ||
|
||||
outcomeType === 'MULTIPLE_CHOICE') && (
|
||||
<AnswersGraph contract={contract} bets={bets} height={graphHeight} />
|
||||
)}
|
||||
|
||||
|
|
|
@ -200,7 +200,9 @@ function IncomeNotificationGroupItem(props: {
|
|||
const { notificationGroup, className } = props
|
||||
const { notifications } = notificationGroup
|
||||
const numSummaryLines = 3
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [expanded, setExpanded] = useState(
|
||||
notifications.length <= numSummaryLines
|
||||
)
|
||||
const [highlighted, setHighlighted] = useState(
|
||||
notifications.some((n) => !n.isSeen)
|
||||
)
|
||||
|
@ -398,7 +400,8 @@ function IncomeNotificationItem(props: {
|
|||
} else if (sourceType === 'tip') {
|
||||
reasonText = !simple ? `tipped you on` : `in tips on`
|
||||
} else if (sourceType === 'betting_streak_bonus') {
|
||||
reasonText = 'for your'
|
||||
if (sourceText && +sourceText === 50) reasonText = '(max) for your'
|
||||
else reasonText = 'for your'
|
||||
} else if (sourceType === 'loan' && sourceText) {
|
||||
reasonText = `of your invested bets returned as a`
|
||||
}
|
||||
|
@ -524,7 +527,7 @@ function IncomeNotificationItem(props: {
|
|||
</span>
|
||||
</div>
|
||||
</Row>
|
||||
<div className={'mt-4 border-b border-gray-300'} />
|
||||
<div className={'border-b border-gray-300 pt-4'} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -541,7 +544,9 @@ function NotificationGroupItem(props: {
|
|||
const isMobile = (width && width < 768) || false
|
||||
const numSummaryLines = 3
|
||||
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [expanded, setExpanded] = useState(
|
||||
notifications.length <= numSummaryLines
|
||||
)
|
||||
const [highlighted, setHighlighted] = useState(
|
||||
notifications.some((n) => !n.isSeen)
|
||||
)
|
||||
|
|
236
web/pages/tournaments/index.tsx
Normal file
236
web/pages/tournaments/index.tsx
Normal file
|
@ -0,0 +1,236 @@
|
|||
import { ClockIcon } from '@heroicons/react/outline'
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
UsersIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import { Contract } from 'common/contract'
|
||||
import { Group } from 'common/group'
|
||||
import dayjs, { Dayjs } from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { keyBy, mapValues, throttle } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { ContractCard } from 'web/components/contract/contract-card'
|
||||
import { DateTimeTooltip } from 'web/components/datetime-tooltip'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { Page } from 'web/components/page'
|
||||
import { SEO } from 'web/components/SEO'
|
||||
import { listContractsByGroupSlug } from 'web/lib/firebase/contracts'
|
||||
import { getGroup, groupPath } from 'web/lib/firebase/groups'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(customParseFormat)
|
||||
const toDate = (d: string) => dayjs(d, 'MMM D, YYYY').tz('America/Los_Angeles')
|
||||
|
||||
type Tourney = {
|
||||
title: string
|
||||
url?: string
|
||||
blurb: string // actual description in the click-through
|
||||
award?: string
|
||||
endTime?: Dayjs
|
||||
groupId: string
|
||||
}
|
||||
|
||||
const Salem = {
|
||||
title: 'CSPI/Salem Forecasting Tournament',
|
||||
blurb: 'Top 5 traders qualify for a UT Austin research fellowship.',
|
||||
url: 'https://salemcenter.manifold.markets/',
|
||||
award: '$25,000',
|
||||
endTime: toDate('Jul 31, 2023'),
|
||||
} as const
|
||||
|
||||
const tourneys: Tourney[] = [
|
||||
{
|
||||
title: 'Cause Exploration Prizes',
|
||||
blurb:
|
||||
'Which new charity ideas will Open Philanthropy find most promising?',
|
||||
award: 'M$100k',
|
||||
endTime: toDate('Sep 9, 2022'),
|
||||
groupId: 'cMcpBQ2p452jEcJD2SFw',
|
||||
},
|
||||
{
|
||||
title: 'Fantasy Football Stock Exchange',
|
||||
blurb: 'How many points will each NFL player score this season?',
|
||||
award: '$2,500',
|
||||
endTime: toDate('Jan 6, 2023'),
|
||||
groupId: 'SxGRqXRpV3RAQKudbcNb',
|
||||
},
|
||||
// {
|
||||
// title: 'Clearer Thinking Regrant Project',
|
||||
// blurb: 'Something amazing',
|
||||
// award: '$10,000',
|
||||
// endTime: toDate('Sep 22, 2022'),
|
||||
// groupId: '2VsVVFGhKtIdJnQRAXVb',
|
||||
// },
|
||||
]
|
||||
|
||||
export async function getStaticProps() {
|
||||
const groupIds = tourneys
|
||||
.map((data) => data.groupId)
|
||||
.filter((id) => id != undefined) as string[]
|
||||
const groups = (await Promise.all(groupIds.map(getGroup)))
|
||||
// Then remove undefined groups
|
||||
.filter(Boolean) as Group[]
|
||||
|
||||
const contracts = await Promise.all(
|
||||
groups.map((g) => listContractsByGroupSlug(g?.slug ?? ''))
|
||||
)
|
||||
|
||||
const markets = Object.fromEntries(groups.map((g, i) => [g.id, contracts[i]]))
|
||||
|
||||
const groupMap = keyBy(groups, 'id')
|
||||
const numPeople = mapValues(groupMap, (g) => g?.memberIds.length)
|
||||
const slugs = mapValues(groupMap, 'slug')
|
||||
|
||||
return { props: { markets, numPeople, slugs }, revalidate: 60 * 10 }
|
||||
}
|
||||
|
||||
export default function TournamentPage(props: {
|
||||
markets: { [groupId: string]: Contract[] }
|
||||
numPeople: { [groupId: string]: number }
|
||||
slugs: { [groupId: string]: string }
|
||||
}) {
|
||||
const { markets = {}, numPeople = {}, slugs = {} } = props
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<SEO
|
||||
title="Tournaments"
|
||||
description="Win money by betting in forecasting touraments on current events, sports, science, and more"
|
||||
/>
|
||||
<Col className="mx-4 mt-4 gap-20 sm:mx-10 xl:w-[125%]">
|
||||
{tourneys.map(({ groupId, ...data }) => (
|
||||
<Section
|
||||
key={groupId}
|
||||
{...data}
|
||||
url={groupPath(slugs[groupId])}
|
||||
ppl={numPeople[groupId] ?? 0}
|
||||
markets={markets[groupId] ?? []}
|
||||
/>
|
||||
))}
|
||||
<Section {...Salem} markets={[]} />
|
||||
</Col>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
function Section(props: {
|
||||
title: string
|
||||
url: string
|
||||
blurb: string
|
||||
award?: string
|
||||
ppl?: number
|
||||
endTime?: Dayjs
|
||||
markets: Contract[]
|
||||
}) {
|
||||
const { title, url, blurb, award, ppl, endTime, markets } = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Link href={url}>
|
||||
<a className="group mb-3 flex flex-wrap justify-between">
|
||||
<h2 className="text-xl font-semibold group-hover:underline md:text-3xl">
|
||||
{title}
|
||||
</h2>
|
||||
<Row className="my-2 items-center gap-4 whitespace-nowrap rounded-full bg-gray-200 px-6">
|
||||
{!!award && <span className="flex items-center">🏆 {award}</span>}
|
||||
{!!ppl && (
|
||||
<span className="flex items-center gap-1">
|
||||
<UsersIcon className="h-4" />
|
||||
{ppl}
|
||||
</span>
|
||||
)}
|
||||
{endTime && (
|
||||
<DateTimeTooltip time={endTime} text="Ends">
|
||||
<span className="flex items-center gap-1">
|
||||
<ClockIcon className="h-4" />
|
||||
{endTime.format('MMM D')}
|
||||
</span>
|
||||
</DateTimeTooltip>
|
||||
)}
|
||||
</Row>
|
||||
</a>
|
||||
</Link>
|
||||
<span>{blurb}</span>
|
||||
<Carousel className="-mx-4 mt-2 sm:-mx-10">
|
||||
<div className="shrink-0 sm:w-6" />
|
||||
{markets.length ? (
|
||||
markets.map((m) => (
|
||||
<ContractCard
|
||||
contract={m}
|
||||
showHotVolume
|
||||
hideGroupLink
|
||||
className="max-h-[200px] w-96 shrink-0"
|
||||
questionClass="line-clamp-3"
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex h-32 w-80 items-center justify-center rounded bg-white text-lg text-gray-700 shadow-md">
|
||||
Coming Soon...
|
||||
</div>
|
||||
)}
|
||||
</Carousel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Carousel(props: { children: ReactNode; className?: string }) {
|
||||
const { children, className } = props
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const th = (f: () => any) => throttle(f, 500, { trailing: false })
|
||||
const scrollLeft = th(() =>
|
||||
ref.current?.scrollBy({ left: -ref.current.clientWidth })
|
||||
)
|
||||
const scrollRight = th(() =>
|
||||
ref.current?.scrollBy({ left: ref.current.clientWidth })
|
||||
)
|
||||
|
||||
const [atFront, setAtFront] = useState(true)
|
||||
const [atBack, setAtBack] = useState(false)
|
||||
const onScroll = throttle(() => {
|
||||
if (ref.current) {
|
||||
const { scrollLeft, clientWidth, scrollWidth } = ref.current
|
||||
setAtFront(scrollLeft < 80)
|
||||
setAtBack(scrollWidth - (clientWidth + scrollLeft) < 80)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(onScroll, [])
|
||||
|
||||
return (
|
||||
<div className={clsx('relative', className)}>
|
||||
<Row
|
||||
className="scrollbar-hide w-full gap-4 overflow-x-auto scroll-smooth"
|
||||
ref={ref}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
{children}
|
||||
</Row>
|
||||
{!atFront && (
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 z-10 flex w-10 cursor-pointer items-center justify-center hover:bg-indigo-100/30"
|
||||
onMouseDown={scrollLeft}
|
||||
>
|
||||
<ChevronLeftIcon className="h-7 w-7 rounded-full bg-indigo-50 text-indigo-700" />
|
||||
</div>
|
||||
)}
|
||||
{!atBack && (
|
||||
<div
|
||||
className="absolute right-0 top-0 bottom-0 z-10 flex w-10 cursor-pointer items-center justify-center hover:bg-indigo-100/30"
|
||||
onMouseDown={scrollRight}
|
||||
>
|
||||
<ChevronRightIcon className="h-7 w-7 rounded-full bg-indigo-50 text-indigo-700" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
18
yarn.lock
18
yarn.lock
|
@ -2919,10 +2919,10 @@
|
|||
lodash.isplainobject "^4.0.6"
|
||||
lodash.merge "^4.6.2"
|
||||
|
||||
"@tiptap/core@2.0.0-beta.181", "@tiptap/core@^2.0.0-beta.181":
|
||||
version "2.0.0-beta.181"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.181.tgz#07aeea26336814ab82eb7f4199b17538187c6fbb"
|
||||
integrity sha512-tbwRqjTVvY9v31TNAH6W0Njhr/OVwI28zWXmH55/USrwyU2CB1iCVfXktZKOhB+8WyvOaBv1JA5YplMIhstYTw==
|
||||
"@tiptap/core@2.0.0-beta.182", "@tiptap/core@^2.0.0-beta.182":
|
||||
version "2.0.0-beta.182"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.182.tgz#d2001e9b765adda95e15d171479860a3349e2d04"
|
||||
integrity sha512-MZGkMGnVnWhBzjvpBNwQ9zBz38ndi3Irbf90uCTSArR0kaCVkW4vmyuPuOXd+0SO8Yv/l5oyDdOCpaG3rnQYfw==
|
||||
dependencies:
|
||||
prosemirror-commands "1.3.0"
|
||||
prosemirror-keymap "1.2.0"
|
||||
|
@ -3099,12 +3099,12 @@
|
|||
"@tiptap/extension-floating-menu" "^2.0.0-beta.56"
|
||||
prosemirror-view "1.26.2"
|
||||
|
||||
"@tiptap/starter-kit@2.0.0-beta.190":
|
||||
version "2.0.0-beta.190"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.0.0-beta.190.tgz#fe0021e29d070fc5707722513a398c8884e15f71"
|
||||
integrity sha512-jaFMkE6mjCHmCJsXUyLiXGYRVDcHF+PbH/5hEu1riUIAT0Hmm7uak5TYsPeuoCVN7P/tmDEBbBRASZ5CzEQpvw==
|
||||
"@tiptap/starter-kit@2.0.0-beta.191":
|
||||
version "2.0.0-beta.191"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.0.0-beta.191.tgz#3f549367f6dbb8cf83f63aa0941722d91d0fd8e7"
|
||||
integrity sha512-YRrBCi9W4jiH/xLTJJOCdD7pL4Wb98Ip8qCJ94RElShDj0O1i5tT9wWlgVWoGIU+CRAds5XENRwZ97sJ+YfYyg==
|
||||
dependencies:
|
||||
"@tiptap/core" "^2.0.0-beta.181"
|
||||
"@tiptap/core" "^2.0.0-beta.182"
|
||||
"@tiptap/extension-blockquote" "^2.0.0-beta.29"
|
||||
"@tiptap/extension-bold" "^2.0.0-beta.28"
|
||||
"@tiptap/extension-bullet-list" "^2.0.0-beta.29"
|
||||
|
|
Loading…
Reference in New Issue
Block a user