diff --git a/common/package.json b/common/package.json index 955e9662..52195398 100644 --- a/common/package.json +++ b/common/package.json @@ -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": { diff --git a/functions/package.json b/functions/package.json index c8f295fc..d5a578de 100644 --- a/functions/package.json +++ b/functions/package.json @@ -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", diff --git a/web/components/answers/answers-graph.tsx b/web/components/answers/answers-graph.tsx index 27152db9..d35132be 100644 --- a/web/components/answers/answers-graph.tsx +++ b/web/components/answers/answers-graph.tsx @@ -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')) { diff --git a/web/components/contract/FeaturedContractBadge.tsx b/web/components/contract/FeaturedContractBadge.tsx deleted file mode 100644 index 5ef34f4a..00000000 --- a/web/components/contract/FeaturedContractBadge.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { SparklesIcon } from '@heroicons/react/solid' - -export function FeaturedContractBadge() { - return ( - - - ) -} diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index ce03108d..6ada9b6f 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -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 ( @@ -104,7 +106,12 @@ export function ContractCard(props: { contract={contract} className={'hidden md:inline-flex'} /> -

+

{question}

@@ -162,11 +169,7 @@ export function ContractCard(props: { showQuickBet ? 'w-[85%]' : 'w-full' )} > - + setOpen(!open)} + onClick={() => + groupToDisplay + ? Router.push(groupPath(groupToDisplay.slug)) + : setOpen(!open) + } > {groupInfo} diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index 7c35a071..5c66aa4c 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -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 ( <> )} -
)} diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 9659d00f..995378ee 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -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' }, diff --git a/web/components/widgets/short-toggle.tsx b/web/components/widgets/short-toggle.tsx index 3c307fda..339de361 100644 --- a/web/components/widgets/short-toggle.tsx +++ b/web/components/widgets/short-toggle.tsx @@ -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 ( { + 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" > Use setting diff --git a/web/lib/icons/trophy-icon.tsx b/web/lib/icons/trophy-icon.tsx new file mode 100644 index 00000000..c845a0af --- /dev/null +++ b/web/lib/icons/trophy-icon.tsx @@ -0,0 +1,27 @@ +export default function TrophyIcon(props: React.SVGProps) { + return ( + + + + + + + + ) +} diff --git a/web/package.json b/web/package.json index db3fdf45..847c7ef5 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index 7ec8daeb..afec84bb 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -166,7 +166,8 @@ export function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { /> )} - {outcomeType === 'FREE_RESPONSE' && ( + {(outcomeType === 'FREE_RESPONSE' || + outcomeType === 'MULTIPLE_CHOICE') && ( )} diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 0fe3b179..bfd18f7f 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -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: { -
+
) @@ -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) ) diff --git a/web/pages/tournaments/index.tsx b/web/pages/tournaments/index.tsx new file mode 100644 index 00000000..b93a9725 --- /dev/null +++ b/web/pages/tournaments/index.tsx @@ -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 ( + + + + {tourneys.map(({ groupId, ...data }) => ( +
+ ))} +
+ + + ) +} + +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 ( +
+ + +

+ {title} +

+ + {!!award && 🏆 {award}} + {!!ppl && ( + + + {ppl} + + )} + {endTime && ( + + + + {endTime.format('MMM D')} + + + )} + +
+ + {blurb} + +
+ {markets.length ? ( + markets.map((m) => ( + + )) + ) : ( +
+ Coming Soon... +
+ )} + +
+ ) +} + +function Carousel(props: { children: ReactNode; className?: string }) { + const { children, className } = props + + const ref = useRef(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 ( +
+ + {children} + + {!atFront && ( +
+ +
+ )} + {!atBack && ( +
+ +
+ )} +
+ ) +} diff --git a/yarn.lock b/yarn.lock index bbc13091..f49b1ccf 100644 --- a/yarn.lock +++ b/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"