Merge branch 'main' into bounty
This commit is contained in:
		
						commit
						97c0a5cfa8
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -30,7 +30,15 @@ const bodySchema = z.object({
 | 
			
		|||
 | 
			
		||||
const binarySchema = z.object({
 | 
			
		||||
  outcome: z.enum(['YES', 'NO']),
 | 
			
		||||
  limitProb: z.number().gte(0.001).lte(0.999).optional(),
 | 
			
		||||
  limitProb: z
 | 
			
		||||
    .number()
 | 
			
		||||
    .gte(0.001)
 | 
			
		||||
    .lte(0.999)
 | 
			
		||||
    .refine(
 | 
			
		||||
      (p) => Math.round(p * 100) === p * 100,
 | 
			
		||||
      'limitProb must be in increments of 0.01 (i.e. whole percentage points)'
 | 
			
		||||
    )
 | 
			
		||||
    .optional(),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const freeResponseSchema = z.object({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import Router from 'next/router'
 | 
			
		||||
import clsx from 'clsx'
 | 
			
		||||
import { MouseEvent } from 'react'
 | 
			
		||||
import { MouseEvent, useState } from 'react'
 | 
			
		||||
import { UserCircleIcon, UserIcon, UsersIcon } from '@heroicons/react/solid'
 | 
			
		||||
 | 
			
		||||
export function Avatar(props: {
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,8 @@ export function Avatar(props: {
 | 
			
		|||
  size?: number | 'xs' | 'sm'
 | 
			
		||||
  className?: string
 | 
			
		||||
}) {
 | 
			
		||||
  const { username, avatarUrl, noLink, size, className } = props
 | 
			
		||||
  const { username, noLink, size, className } = props
 | 
			
		||||
  const [avatarUrl, setAvatarUrl] = useState(props.avatarUrl)
 | 
			
		||||
  const s = size == 'xs' ? 6 : size === 'sm' ? 8 : size || 10
 | 
			
		||||
 | 
			
		||||
  const onClick =
 | 
			
		||||
| 
						 | 
				
			
			@ -35,6 +36,11 @@ export function Avatar(props: {
 | 
			
		|||
      src={avatarUrl}
 | 
			
		||||
      onClick={onClick}
 | 
			
		||||
      alt={username}
 | 
			
		||||
      onError={() => {
 | 
			
		||||
        // If the image doesn't load, clear the avatarUrl to show the default
 | 
			
		||||
        // Mostly for localhost, when getting a 403 from googleusercontent
 | 
			
		||||
        setAvatarUrl('')
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  ) : (
 | 
			
		||||
    <UserCircleIcon
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,11 +4,7 @@ import { SearchOptions } from '@algolia/client-search'
 | 
			
		|||
 | 
			
		||||
import { Contract } from 'common/contract'
 | 
			
		||||
import { User } from 'common/user'
 | 
			
		||||
import {
 | 
			
		||||
  QuerySortOptions,
 | 
			
		||||
  Sort,
 | 
			
		||||
  useQueryAndSortParams,
 | 
			
		||||
} from '../hooks/use-sort-and-query-params'
 | 
			
		||||
import { Sort, useQuery, useSort } from '../hooks/use-sort-and-query-params'
 | 
			
		||||
import {
 | 
			
		||||
  ContractHighlightOptions,
 | 
			
		||||
  ContractsGrid,
 | 
			
		||||
| 
						 | 
				
			
			@ -24,11 +20,25 @@ import ContractSearchFirestore from 'web/pages/contract-search-firestore'
 | 
			
		|||
import { useMemberGroups } from 'web/hooks/use-group'
 | 
			
		||||
import { NEW_USER_GROUP_SLUGS } from 'common/group'
 | 
			
		||||
import { PillButton } from './buttons/pill-button'
 | 
			
		||||
import { sortBy } from 'lodash'
 | 
			
		||||
import { debounce, sortBy } from 'lodash'
 | 
			
		||||
import { DEFAULT_CATEGORY_GROUPS } from 'common/categories'
 | 
			
		||||
import { Col } from './layout/col'
 | 
			
		||||
import { safeLocalStorage } from 'web/lib/util/local'
 | 
			
		||||
import clsx from 'clsx'
 | 
			
		||||
 | 
			
		||||
// TODO: this obviously doesn't work with SSR, common sense would suggest
 | 
			
		||||
// that we should save things like this in cookies so the server has them
 | 
			
		||||
 | 
			
		||||
const MARKETS_SORT = 'markets_sort'
 | 
			
		||||
 | 
			
		||||
function setSavedSort(s: Sort) {
 | 
			
		||||
  safeLocalStorage()?.setItem(MARKETS_SORT, s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getSavedSort() {
 | 
			
		||||
  return safeLocalStorage()?.getItem(MARKETS_SORT) as Sort | null | undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const searchClient = algoliasearch(
 | 
			
		||||
  'GJQPAYENIF',
 | 
			
		||||
  '75c28fc084a80e1129d427d470cf41a3'
 | 
			
		||||
| 
						 | 
				
			
			@ -47,7 +57,6 @@ const sortOptions = [
 | 
			
		|||
  { label: 'Close date', value: 'close-date' },
 | 
			
		||||
  { label: 'Resolve date', value: 'resolve-date' },
 | 
			
		||||
]
 | 
			
		||||
export const DEFAULT_SORT = 'score'
 | 
			
		||||
 | 
			
		||||
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +77,8 @@ type AdditionalFilter = {
 | 
			
		|||
 | 
			
		||||
export function ContractSearch(props: {
 | 
			
		||||
  user?: User | null
 | 
			
		||||
  querySortOptions?: { defaultFilter?: filter } & QuerySortOptions
 | 
			
		||||
  defaultSort?: Sort
 | 
			
		||||
  defaultFilter?: filter
 | 
			
		||||
  additionalFilter?: AdditionalFilter
 | 
			
		||||
  highlightOptions?: ContractHighlightOptions
 | 
			
		||||
  onContractClick?: (contract: Contract) => void
 | 
			
		||||
| 
						 | 
				
			
			@ -79,10 +89,13 @@ export function ContractSearch(props: {
 | 
			
		|||
    hideQuickBet?: boolean
 | 
			
		||||
  }
 | 
			
		||||
  headerClassName?: string
 | 
			
		||||
  useQuerySortLocalStorage?: boolean
 | 
			
		||||
  useQuerySortUrlParams?: boolean
 | 
			
		||||
}) {
 | 
			
		||||
  const {
 | 
			
		||||
    user,
 | 
			
		||||
    querySortOptions,
 | 
			
		||||
    defaultSort,
 | 
			
		||||
    defaultFilter,
 | 
			
		||||
    additionalFilter,
 | 
			
		||||
    onContractClick,
 | 
			
		||||
    overrideGridClassName,
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +103,8 @@ export function ContractSearch(props: {
 | 
			
		|||
    cardHideOptions,
 | 
			
		||||
    highlightOptions,
 | 
			
		||||
    headerClassName,
 | 
			
		||||
    useQuerySortLocalStorage,
 | 
			
		||||
    useQuerySortUrlParams,
 | 
			
		||||
  } = props
 | 
			
		||||
 | 
			
		||||
  const [numPages, setNumPages] = useState(1)
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +139,7 @@ export function ContractSearch(props: {
 | 
			
		|||
          setNumPages(results.nbPages)
 | 
			
		||||
          if (freshQuery) {
 | 
			
		||||
            setPages([newPage])
 | 
			
		||||
            window.scrollTo(0, 0)
 | 
			
		||||
          } else {
 | 
			
		||||
            setPages((pages) => [...pages, newPage])
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			@ -132,31 +148,33 @@ export function ContractSearch(props: {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onSearchParametersChanged = useRef(
 | 
			
		||||
    debounce((params) => {
 | 
			
		||||
      searchParameters.current = params
 | 
			
		||||
      performQuery(true)
 | 
			
		||||
    }, 100)
 | 
			
		||||
  ).current
 | 
			
		||||
 | 
			
		||||
  const contracts = pages
 | 
			
		||||
    .flat()
 | 
			
		||||
    .filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id))
 | 
			
		||||
 | 
			
		||||
  if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
 | 
			
		||||
    return (
 | 
			
		||||
      <ContractSearchFirestore
 | 
			
		||||
        querySortOptions={querySortOptions}
 | 
			
		||||
        additionalFilter={additionalFilter}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
    return <ContractSearchFirestore additionalFilter={additionalFilter} />
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Col className="h-full">
 | 
			
		||||
      <ContractSearchControls
 | 
			
		||||
        className={headerClassName}
 | 
			
		||||
        defaultSort={defaultSort}
 | 
			
		||||
        defaultFilter={defaultFilter}
 | 
			
		||||
        additionalFilter={additionalFilter}
 | 
			
		||||
        hideOrderSelector={hideOrderSelector}
 | 
			
		||||
        querySortOptions={querySortOptions}
 | 
			
		||||
        useQuerySortLocalStorage={useQuerySortLocalStorage}
 | 
			
		||||
        useQuerySortUrlParams={useQuerySortUrlParams}
 | 
			
		||||
        user={user}
 | 
			
		||||
        onSearchParametersChanged={(params) => {
 | 
			
		||||
          searchParameters.current = params
 | 
			
		||||
          performQuery(true)
 | 
			
		||||
        }}
 | 
			
		||||
        onSearchParametersChanged={onSearchParametersChanged}
 | 
			
		||||
      />
 | 
			
		||||
      <ContractsGrid
 | 
			
		||||
        contracts={pages.length === 0 ? undefined : contracts}
 | 
			
		||||
| 
						 | 
				
			
			@ -173,23 +191,40 @@ export function ContractSearch(props: {
 | 
			
		|||
 | 
			
		||||
function ContractSearchControls(props: {
 | 
			
		||||
  className?: string
 | 
			
		||||
  defaultSort?: Sort
 | 
			
		||||
  defaultFilter?: filter
 | 
			
		||||
  additionalFilter?: AdditionalFilter
 | 
			
		||||
  hideOrderSelector?: boolean
 | 
			
		||||
  onSearchParametersChanged: (params: SearchParameters) => void
 | 
			
		||||
  querySortOptions?: { defaultFilter?: filter } & QuerySortOptions
 | 
			
		||||
  useQuerySortLocalStorage?: boolean
 | 
			
		||||
  useQuerySortUrlParams?: boolean
 | 
			
		||||
  user?: User | null
 | 
			
		||||
}) {
 | 
			
		||||
  const {
 | 
			
		||||
    className,
 | 
			
		||||
    defaultSort,
 | 
			
		||||
    defaultFilter,
 | 
			
		||||
    additionalFilter,
 | 
			
		||||
    hideOrderSelector,
 | 
			
		||||
    onSearchParametersChanged,
 | 
			
		||||
    querySortOptions,
 | 
			
		||||
    useQuerySortLocalStorage,
 | 
			
		||||
    useQuerySortUrlParams,
 | 
			
		||||
    user,
 | 
			
		||||
  } = props
 | 
			
		||||
 | 
			
		||||
  const { query, setQuery, sort, setSort } =
 | 
			
		||||
    useQueryAndSortParams(querySortOptions)
 | 
			
		||||
  const savedSort = useQuerySortLocalStorage ? getSavedSort() : null
 | 
			
		||||
  const initialSort = savedSort ?? defaultSort ?? 'score'
 | 
			
		||||
  const querySortOpts = { useUrl: !!useQuerySortUrlParams }
 | 
			
		||||
  const [sort, setSort] = useSort(initialSort, querySortOpts)
 | 
			
		||||
  const [query, setQuery] = useQuery('', querySortOpts)
 | 
			
		||||
  const [filter, setFilter] = useState<filter>(defaultFilter ?? 'open')
 | 
			
		||||
  const [pillFilter, setPillFilter] = useState<string | undefined>(undefined)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (useQuerySortLocalStorage) {
 | 
			
		||||
      setSavedSort(sort)
 | 
			
		||||
    }
 | 
			
		||||
  }, [sort])
 | 
			
		||||
 | 
			
		||||
  const follows = useFollows(user?.id)
 | 
			
		||||
  const memberGroups = (useMemberGroups(user?.id) ?? []).filter(
 | 
			
		||||
| 
						 | 
				
			
			@ -208,12 +243,6 @@ function ContractSearchControls(props: {
 | 
			
		|||
  const pillGroups: { name: string; slug: string }[] =
 | 
			
		||||
    memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS
 | 
			
		||||
 | 
			
		||||
  const [filter, setFilter] = useState<filter>(
 | 
			
		||||
    querySortOptions?.defaultFilter ?? 'open'
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const [pillFilter, setPillFilter] = useState<string | undefined>(undefined)
 | 
			
		||||
 | 
			
		||||
  const additionalFilters = [
 | 
			
		||||
    additionalFilter?.creatorId
 | 
			
		||||
      ? `creatorId:${additionalFilter.creatorId}`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -78,7 +78,7 @@ export function ContractCard(props: {
 | 
			
		|||
        className
 | 
			
		||||
      )}
 | 
			
		||||
    >
 | 
			
		||||
      <Col className="group relative flex-1 gap-3 py-4 pl-6">
 | 
			
		||||
      <Col className="group relative flex-1 gap-3 py-4 pb-12  pl-6">
 | 
			
		||||
        {onClick ? (
 | 
			
		||||
          <a
 | 
			
		||||
            className="absolute top-0 left-0 right-0 bottom-0"
 | 
			
		||||
| 
						 | 
				
			
			@ -106,7 +106,10 @@ export function ContractCard(props: {
 | 
			
		|||
            />
 | 
			
		||||
          </Link>
 | 
			
		||||
        )}
 | 
			
		||||
        <AvatarDetails contract={contract} />
 | 
			
		||||
        <AvatarDetails
 | 
			
		||||
          contract={contract}
 | 
			
		||||
          className={'hidden md:inline-flex'}
 | 
			
		||||
        />
 | 
			
		||||
        <p
 | 
			
		||||
          className="break-words font-semibold text-indigo-700 group-hover:underline group-hover:decoration-indigo-400 group-hover:decoration-2"
 | 
			
		||||
          style={{ /* For iOS safari */ wordBreak: 'break-word' }}
 | 
			
		||||
| 
						 | 
				
			
			@ -125,13 +128,19 @@ export function ContractCard(props: {
 | 
			
		|||
          ) : (
 | 
			
		||||
            <FreeResponseTopAnswer contract={contract} truncate="long" />
 | 
			
		||||
          ))}
 | 
			
		||||
 | 
			
		||||
        <MiscDetails
 | 
			
		||||
          contract={contract}
 | 
			
		||||
          showHotVolume={showHotVolume}
 | 
			
		||||
          showTime={showTime}
 | 
			
		||||
          hideGroupLink={hideGroupLink}
 | 
			
		||||
        />
 | 
			
		||||
        <Row className={'absolute bottom-3 gap-2 md:gap-0'}>
 | 
			
		||||
          <AvatarDetails
 | 
			
		||||
            contract={contract}
 | 
			
		||||
            short={true}
 | 
			
		||||
            className={'block md:hidden'}
 | 
			
		||||
          />
 | 
			
		||||
          <MiscDetails
 | 
			
		||||
            contract={contract}
 | 
			
		||||
            showHotVolume={showHotVolume}
 | 
			
		||||
            showTime={showTime}
 | 
			
		||||
            hideGroupLink={hideGroupLink}
 | 
			
		||||
          />
 | 
			
		||||
        </Row>
 | 
			
		||||
      </Col>
 | 
			
		||||
      {showQuickBet ? (
 | 
			
		||||
        <QuickBet contract={contract} user={user} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,6 +34,7 @@ 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'
 | 
			
		||||
 | 
			
		||||
export type ShowTime = 'resolve-date' | 'close-date'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -83,9 +84,8 @@ export function MiscDetails(props: {
 | 
			
		|||
      {!hideGroupLink && groupLinks && groupLinks.length > 0 && (
 | 
			
		||||
        <SiteLink
 | 
			
		||||
          href={groupPath(groupLinks[0].slug)}
 | 
			
		||||
          className="line-clamp-1 text-sm text-gray-400"
 | 
			
		||||
          className="truncate text-sm text-gray-400"
 | 
			
		||||
        >
 | 
			
		||||
          <UserGroupIcon className="mx-1 mb-0.5 inline h-4 w-4 shrink-0" />
 | 
			
		||||
          {groupLinks[0].name}
 | 
			
		||||
        </SiteLink>
 | 
			
		||||
      )}
 | 
			
		||||
| 
						 | 
				
			
			@ -93,18 +93,24 @@ export function MiscDetails(props: {
 | 
			
		|||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function AvatarDetails(props: { contract: Contract }) {
 | 
			
		||||
  const { contract } = props
 | 
			
		||||
export function AvatarDetails(props: {
 | 
			
		||||
  contract: Contract
 | 
			
		||||
  className?: string
 | 
			
		||||
  short?: boolean
 | 
			
		||||
}) {
 | 
			
		||||
  const { contract, short, className } = props
 | 
			
		||||
  const { creatorName, creatorUsername } = contract
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Row className="items-center gap-2 text-sm text-gray-400">
 | 
			
		||||
    <Row
 | 
			
		||||
      className={clsx('items-center gap-2 text-sm text-gray-400', className)}
 | 
			
		||||
    >
 | 
			
		||||
      <Avatar
 | 
			
		||||
        username={creatorUsername}
 | 
			
		||||
        avatarUrl={contract.creatorAvatarUrl}
 | 
			
		||||
        size={6}
 | 
			
		||||
      />
 | 
			
		||||
      <UserLink name={creatorName} username={creatorUsername} />
 | 
			
		||||
      <UserLink name={creatorName} username={creatorUsername} short={short} />
 | 
			
		||||
    </Row>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -106,11 +106,8 @@ export function CreatorContractsList(props: {
 | 
			
		|||
  return (
 | 
			
		||||
    <ContractSearch
 | 
			
		||||
      user={user}
 | 
			
		||||
      querySortOptions={{
 | 
			
		||||
        defaultSort: 'newest',
 | 
			
		||||
        defaultFilter: 'all',
 | 
			
		||||
        shouldLoadFromStorage: false,
 | 
			
		||||
      }}
 | 
			
		||||
      defaultSort="newest"
 | 
			
		||||
      defaultFilter="all"
 | 
			
		||||
      additionalFilter={{
 | 
			
		||||
        creatorId: creator.id,
 | 
			
		||||
      }}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,17 +51,10 @@ export function ShareModal(props: {
 | 
			
		|||
          color="gradient"
 | 
			
		||||
          className={'mb-2 flex max-w-xs self-center'}
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            if (window.navigator.share) {
 | 
			
		||||
              window.navigator.share({
 | 
			
		||||
                url: shareUrl,
 | 
			
		||||
                title: contract.question,
 | 
			
		||||
              })
 | 
			
		||||
            } else {
 | 
			
		||||
              copyToClipboard(shareUrl)
 | 
			
		||||
              toast.success('Link copied!', {
 | 
			
		||||
                icon: linkIcon,
 | 
			
		||||
              })
 | 
			
		||||
            }
 | 
			
		||||
            copyToClipboard(shareUrl)
 | 
			
		||||
            toast.success('Link copied!', {
 | 
			
		||||
              icon: linkIcon,
 | 
			
		||||
            })
 | 
			
		||||
            track('copy share link')
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +66,7 @@ export function ShareModal(props: {
 | 
			
		|||
            className="self-start"
 | 
			
		||||
            tweetText={getTweetText(contract, shareUrl)}
 | 
			
		||||
          />
 | 
			
		||||
          <ShareEmbedButton contract={contract} toastClassName={'-left-20'} />
 | 
			
		||||
          <ShareEmbedButton contract={contract} />
 | 
			
		||||
          <DuplicateContractButton contract={contract} />
 | 
			
		||||
        </Row>
 | 
			
		||||
      </Col>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,7 +69,6 @@ export function MarketModal(props: {
 | 
			
		|||
              'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
 | 
			
		||||
            }
 | 
			
		||||
            cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
 | 
			
		||||
            querySortOptions={{ disableQueryString: true }}
 | 
			
		||||
            highlightOptions={{
 | 
			
		||||
              contractIds: contracts.map((c) => c.id),
 | 
			
		||||
              highlightClassName:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,12 @@
 | 
			
		|||
import React, { Fragment } from 'react'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { CodeIcon } from '@heroicons/react/outline'
 | 
			
		||||
import { Menu, Transition } from '@headlessui/react'
 | 
			
		||||
import { Menu } from '@headlessui/react'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
 | 
			
		||||
import { Contract } from 'common/contract'
 | 
			
		||||
import { contractPath } from 'web/lib/firebase/contracts'
 | 
			
		||||
import { DOMAIN } from 'common/envs/constants'
 | 
			
		||||
import { copyToClipboard } from 'web/lib/util/copy'
 | 
			
		||||
import { ToastClipboard } from 'web/components/toast-clipboard'
 | 
			
		||||
import { track } from 'web/lib/service/analytics'
 | 
			
		||||
 | 
			
		||||
export function embedCode(contract: Contract) {
 | 
			
		||||
| 
						 | 
				
			
			@ -16,11 +16,10 @@ export function embedCode(contract: Contract) {
 | 
			
		|||
  return `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ShareEmbedButton(props: {
 | 
			
		||||
  contract: Contract
 | 
			
		||||
  toastClassName?: string
 | 
			
		||||
}) {
 | 
			
		||||
  const { contract, toastClassName } = props
 | 
			
		||||
export function ShareEmbedButton(props: { contract: Contract }) {
 | 
			
		||||
  const { contract } = props
 | 
			
		||||
 | 
			
		||||
  const codeIcon = <CodeIcon className="mr-1.5 h-4 w-4" aria-hidden="true" />
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Menu
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +27,9 @@ export function ShareEmbedButton(props: {
 | 
			
		|||
      className="relative z-10 flex-shrink-0"
 | 
			
		||||
      onMouseUp={() => {
 | 
			
		||||
        copyToClipboard(embedCode(contract))
 | 
			
		||||
        toast.success('Embed code copied!', {
 | 
			
		||||
          icon: codeIcon,
 | 
			
		||||
        })
 | 
			
		||||
        track('copy embed code')
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
| 
						 | 
				
			
			@ -39,25 +41,9 @@ export function ShareEmbedButton(props: {
 | 
			
		|||
          color: '#9ca3af', // text-gray-400
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <CodeIcon className="mr-1.5 h-4 w-4" aria-hidden="true" />
 | 
			
		||||
        {codeIcon}
 | 
			
		||||
        Embed
 | 
			
		||||
      </Menu.Button>
 | 
			
		||||
 | 
			
		||||
      <Transition
 | 
			
		||||
        as={Fragment}
 | 
			
		||||
        enter="transition ease-out duration-100"
 | 
			
		||||
        enterFrom="transform opacity-0 scale-95"
 | 
			
		||||
        enterTo="transform opacity-100 scale-100"
 | 
			
		||||
        leave="transition ease-in duration-75"
 | 
			
		||||
        leaveFrom="transform opacity-100 scale-100"
 | 
			
		||||
        leaveTo="transform opacity-0 scale-95"
 | 
			
		||||
      >
 | 
			
		||||
        <Menu.Items>
 | 
			
		||||
          <Menu.Item>
 | 
			
		||||
            <ToastClipboard className={toastClassName} />
 | 
			
		||||
          </Menu.Item>
 | 
			
		||||
        </Menu.Items>
 | 
			
		||||
      </Transition>
 | 
			
		||||
    </Menu>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,16 +34,25 @@ export function UserLink(props: {
 | 
			
		|||
  username: string
 | 
			
		||||
  showUsername?: boolean
 | 
			
		||||
  className?: string
 | 
			
		||||
  justFirstName?: boolean
 | 
			
		||||
  short?: boolean
 | 
			
		||||
}) {
 | 
			
		||||
  const { name, username, showUsername, className, justFirstName } = props
 | 
			
		||||
 | 
			
		||||
  const { name, username, showUsername, className, short } = props
 | 
			
		||||
  const firstName = name.split(' ')[0]
 | 
			
		||||
  const maxLength = 10
 | 
			
		||||
  const shortName =
 | 
			
		||||
    firstName.length >= 3
 | 
			
		||||
      ? firstName.length < maxLength
 | 
			
		||||
        ? firstName
 | 
			
		||||
        : firstName.substring(0, maxLength - 3) + '...'
 | 
			
		||||
      : name.length > maxLength
 | 
			
		||||
      ? name.substring(0, maxLength) + '...'
 | 
			
		||||
      : name
 | 
			
		||||
  return (
 | 
			
		||||
    <SiteLink
 | 
			
		||||
      href={`/${username}`}
 | 
			
		||||
      className={clsx('z-10 truncate', className)}
 | 
			
		||||
    >
 | 
			
		||||
      {justFirstName ? name.split(' ')[0] : name}
 | 
			
		||||
      {short ? shortName : name}
 | 
			
		||||
      {showUsername && ` (@${username})`}
 | 
			
		||||
    </SiteLink>
 | 
			
		||||
  )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,5 @@
 | 
			
		|||
import { debounce } from 'lodash'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { useEffect, useMemo, useState } from 'react'
 | 
			
		||||
import { DEFAULT_SORT } from 'web/components/contract-search'
 | 
			
		||||
 | 
			
		||||
const MARKETS_SORT = 'markets_sort'
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
import { NextRouter, useRouter } from 'next/router'
 | 
			
		||||
 | 
			
		||||
export type Sort =
 | 
			
		||||
  | 'newest'
 | 
			
		||||
| 
						 | 
				
			
			@ -15,92 +11,55 @@ export type Sort =
 | 
			
		|||
  | 'last-updated'
 | 
			
		||||
  | 'score'
 | 
			
		||||
 | 
			
		||||
export function getSavedSort() {
 | 
			
		||||
  // TODO: this obviously doesn't work with SSR, common sense would suggest
 | 
			
		||||
  // that we should save things like this in cookies so the server has them
 | 
			
		||||
  if (typeof window !== 'undefined') {
 | 
			
		||||
    return localStorage.getItem(MARKETS_SORT) as Sort | null
 | 
			
		||||
  } else {
 | 
			
		||||
    return null
 | 
			
		||||
type UpdatedQueryParams = { [k: string]: string }
 | 
			
		||||
type QuerySortOpts = { useUrl: boolean }
 | 
			
		||||
 | 
			
		||||
function withURLParams(location: Location, params: UpdatedQueryParams) {
 | 
			
		||||
  const newParams = new URLSearchParams(location.search)
 | 
			
		||||
  for (const [k, v] of Object.entries(params)) {
 | 
			
		||||
    if (!v) {
 | 
			
		||||
      newParams.delete(k)
 | 
			
		||||
    } else {
 | 
			
		||||
      newParams.set(k, v)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const newUrl = new URL(location.href)
 | 
			
		||||
  newUrl.search = newParams.toString()
 | 
			
		||||
  return newUrl
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface QuerySortOptions {
 | 
			
		||||
  defaultSort?: Sort
 | 
			
		||||
  shouldLoadFromStorage?: boolean
 | 
			
		||||
  /** Use normal react state instead of url query string */
 | 
			
		||||
  disableQueryString?: boolean
 | 
			
		||||
function updateURL(params: UpdatedQueryParams) {
 | 
			
		||||
  // see relevant discussion here https://github.com/vercel/next.js/discussions/18072
 | 
			
		||||
  const url = withURLParams(window.location, params).toString()
 | 
			
		||||
  const updatedState = { ...window.history.state, as: url, url }
 | 
			
		||||
  window.history.replaceState(updatedState, '', url)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useQueryAndSortParams({
 | 
			
		||||
  defaultSort = DEFAULT_SORT,
 | 
			
		||||
  shouldLoadFromStorage = true,
 | 
			
		||||
  disableQueryString,
 | 
			
		||||
}: QuerySortOptions = {}) {
 | 
			
		||||
function getStringURLParam(router: NextRouter, k: string) {
 | 
			
		||||
  const v = router.query[k]
 | 
			
		||||
  return typeof v === 'string' ? v : null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useQuery(defaultQuery: string, opts?: QuerySortOpts) {
 | 
			
		||||
  const useUrl = opts?.useUrl ?? false
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
 | 
			
		||||
  const { s: sort, q: query } = router.query as {
 | 
			
		||||
    q?: string
 | 
			
		||||
    s?: Sort
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const setSort = (sort: Sort | undefined) => {
 | 
			
		||||
    router.replace({ query: { ...router.query, s: sort } }, undefined, {
 | 
			
		||||
      shallow: true,
 | 
			
		||||
    })
 | 
			
		||||
    if (shouldLoadFromStorage) {
 | 
			
		||||
      localStorage.setItem(MARKETS_SORT, sort || '')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [queryState, setQueryState] = useState(query)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setQueryState(query)
 | 
			
		||||
  }, [query])
 | 
			
		||||
 | 
			
		||||
  // Debounce router query update.
 | 
			
		||||
  const pushQuery = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      debounce((query: string | undefined) => {
 | 
			
		||||
        const queryObj = { ...router.query, q: query }
 | 
			
		||||
        if (!query) delete queryObj.q
 | 
			
		||||
        router.replace({ query: queryObj }, undefined, {
 | 
			
		||||
          shallow: true,
 | 
			
		||||
        })
 | 
			
		||||
      }, 100),
 | 
			
		||||
    [router]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const setQuery = (query: string | undefined) => {
 | 
			
		||||
    setQueryState(query)
 | 
			
		||||
    if (!disableQueryString) {
 | 
			
		||||
      pushQuery(query)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // If there's no sort option, then set the one from localstorage
 | 
			
		||||
    if (router.isReady && !sort && shouldLoadFromStorage) {
 | 
			
		||||
      const localSort = localStorage.getItem(MARKETS_SORT) as Sort
 | 
			
		||||
      if (localSort && localSort !== defaultSort) {
 | 
			
		||||
        // Use replace to not break navigating back.
 | 
			
		||||
        router.replace(
 | 
			
		||||
          { query: { ...router.query, s: localSort } },
 | 
			
		||||
          undefined,
 | 
			
		||||
          { shallow: true }
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // use normal state if querydisableQueryString
 | 
			
		||||
  const [sortState, setSortState] = useState(defaultSort)
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    sort: disableQueryString ? sortState : sort ?? defaultSort,
 | 
			
		||||
    query: queryState ?? '',
 | 
			
		||||
    setSort: disableQueryString ? setSortState : setSort,
 | 
			
		||||
    setQuery,
 | 
			
		||||
  const initialQuery = useUrl ? getStringURLParam(router, 'q') : null
 | 
			
		||||
  const [query, setQuery] = useState(initialQuery ?? defaultQuery)
 | 
			
		||||
  if (!useUrl) {
 | 
			
		||||
    return [query, setQuery] as const
 | 
			
		||||
  } else {
 | 
			
		||||
    return [query, (q: string) => (setQuery(q), updateURL({ q }))] as const
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useSort(defaultSort: Sort, opts?: QuerySortOpts) {
 | 
			
		||||
  const useUrl = opts?.useUrl ?? false
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const initialSort = useUrl ? (getStringURLParam(router, 's') as Sort) : null
 | 
			
		||||
  const [sort, setSort] = useState(initialSort ?? defaultSort)
 | 
			
		||||
  if (!useUrl) {
 | 
			
		||||
    return [sort, setSort] as const
 | 
			
		||||
  } else {
 | 
			
		||||
    return [sort, (s: Sort) => (setSort(s), updateURL({ s }))] as const
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,16 +3,11 @@ import { searchInAny } from 'common/util/parse'
 | 
			
		|||
import { sortBy } from 'lodash'
 | 
			
		||||
import { ContractsGrid } from 'web/components/contract/contracts-grid'
 | 
			
		||||
import { useContracts } from 'web/hooks/use-contracts'
 | 
			
		||||
import {
 | 
			
		||||
  QuerySortOptions,
 | 
			
		||||
  Sort,
 | 
			
		||||
  useQueryAndSortParams,
 | 
			
		||||
} from 'web/hooks/use-sort-and-query-params'
 | 
			
		||||
import { Sort, useQuery, useSort } from 'web/hooks/use-sort-and-query-params'
 | 
			
		||||
 | 
			
		||||
const MAX_CONTRACTS_RENDERED = 100
 | 
			
		||||
 | 
			
		||||
export default function ContractSearchFirestore(props: {
 | 
			
		||||
  querySortOptions?: QuerySortOptions
 | 
			
		||||
  additionalFilter?: {
 | 
			
		||||
    creatorId?: string
 | 
			
		||||
    tag?: string
 | 
			
		||||
| 
						 | 
				
			
			@ -21,10 +16,9 @@ export default function ContractSearchFirestore(props: {
 | 
			
		|||
  }
 | 
			
		||||
}) {
 | 
			
		||||
  const contracts = useContracts()
 | 
			
		||||
  const { querySortOptions, additionalFilter } = props
 | 
			
		||||
 | 
			
		||||
  const { query, setQuery, sort, setSort } =
 | 
			
		||||
    useQueryAndSortParams(querySortOptions)
 | 
			
		||||
  const { additionalFilter } = props
 | 
			
		||||
  const [query, setQuery] = useQuery('', { useUrl: true })
 | 
			
		||||
  const [sort, setSort] = useSort('score', { useUrl: true })
 | 
			
		||||
 | 
			
		||||
  let matches = (contracts ?? []).filter((c) =>
 | 
			
		||||
    searchInAny(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,6 @@ import { CreateQuestionButton } from 'web/components/create-question-button'
 | 
			
		|||
import React, { useState } from 'react'
 | 
			
		||||
import { LoadingIndicator } from 'web/components/loading-indicator'
 | 
			
		||||
import { Modal } from 'web/components/layout/modal'
 | 
			
		||||
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'
 | 
			
		||||
| 
						 | 
				
			
			@ -196,11 +195,8 @@ export default function GroupPage(props: {
 | 
			
		|||
  const questionsTab = (
 | 
			
		||||
    <ContractSearch
 | 
			
		||||
      user={user}
 | 
			
		||||
      querySortOptions={{
 | 
			
		||||
        shouldLoadFromStorage: true,
 | 
			
		||||
        defaultSort: getSavedSort() ?? 'newest',
 | 
			
		||||
        defaultFilter: 'open',
 | 
			
		||||
      }}
 | 
			
		||||
      defaultSort={'newest'}
 | 
			
		||||
      defaultFilter={'open'}
 | 
			
		||||
      additionalFilter={{ groupSlug: group.slug }}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,8 +4,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, DEFAULT_SORT } from 'web/components/contract-search'
 | 
			
		||||
import { ContractSearch } from 'web/components/contract-search'
 | 
			
		||||
import { Contract } from 'common/contract'
 | 
			
		||||
import { User } from 'common/user'
 | 
			
		||||
import { ContractPageContent } from './[username]/[contractSlug]'
 | 
			
		||||
| 
						 | 
				
			
			@ -35,10 +34,8 @@ const Home = (props: { auth: { user: User } }) => {
 | 
			
		|||
        <Col className="mx-auto w-full p-2">
 | 
			
		||||
          <ContractSearch
 | 
			
		||||
            user={user}
 | 
			
		||||
            querySortOptions={{
 | 
			
		||||
              shouldLoadFromStorage: true,
 | 
			
		||||
              defaultSort: getSavedSort() ?? DEFAULT_SORT,
 | 
			
		||||
            }}
 | 
			
		||||
            useQuerySortLocalStorage={true}
 | 
			
		||||
            useQuerySortUrlParams={true}
 | 
			
		||||
            onContractClick={(c) => {
 | 
			
		||||
              // Show contract without navigating to contract page.
 | 
			
		||||
              setContract(c)
 | 
			
		||||
| 
						 | 
				
			
			@ -104,13 +101,19 @@ const useContractPage = () => {
 | 
			
		|||
 | 
			
		||||
    window.history.pushState = function () {
 | 
			
		||||
      // eslint-disable-next-line prefer-rest-params
 | 
			
		||||
      pushState.apply(history, arguments as any)
 | 
			
		||||
      const args = [...(arguments as any)] as any
 | 
			
		||||
      // Discard NextJS router state.
 | 
			
		||||
      args[0] = null
 | 
			
		||||
      pushState.apply(history, args)
 | 
			
		||||
      updateContract()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.history.replaceState = function () {
 | 
			
		||||
      // eslint-disable-next-line prefer-rest-params
 | 
			
		||||
      replaceState.apply(history, arguments as any)
 | 
			
		||||
      const args = [...(arguments as any)] as any
 | 
			
		||||
      // Discard NextJS router state.
 | 
			
		||||
      args[0] = null
 | 
			
		||||
      replaceState.apply(history, args)
 | 
			
		||||
      updateContract()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,6 +40,7 @@ import { safeLocalStorage } from 'web/lib/util/local'
 | 
			
		|||
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
 | 
			
		||||
import { SiteLink } from 'web/components/site-link'
 | 
			
		||||
import { NotificationSettings } from 'web/components/NotificationSettings'
 | 
			
		||||
import { SEO } from 'web/components/SEO'
 | 
			
		||||
 | 
			
		||||
export const NOTIFICATIONS_PER_PAGE = 30
 | 
			
		||||
const MULTIPLE_USERS_KEY = 'multipleUsers'
 | 
			
		||||
| 
						 | 
				
			
			@ -68,6 +69,8 @@ export default function Notifications(props: {
 | 
			
		|||
    <Page>
 | 
			
		||||
      <div className={'px-2 pt-4 sm:px-4 lg:pt-0'}>
 | 
			
		||||
        <Title text={'Notifications'} className={'hidden md:block'} />
 | 
			
		||||
        <SEO title="Notifications" description="Manifold user notifications" />
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
          <Tabs
 | 
			
		||||
            currentPageForAnalytics={'notifications'}
 | 
			
		||||
| 
						 | 
				
			
			@ -437,7 +440,7 @@ function IncomeNotificationItem(props: {
 | 
			
		|||
                    name={sourceUserName || ''}
 | 
			
		||||
                    username={sourceUserUsername || ''}
 | 
			
		||||
                    className={'mr-1 flex-shrink-0'}
 | 
			
		||||
                    justFirstName={true}
 | 
			
		||||
                    short={true}
 | 
			
		||||
                  />
 | 
			
		||||
                ))}
 | 
			
		||||
              {getReasonForShowingIncomeNotification(false)} {' on'}
 | 
			
		||||
| 
						 | 
				
			
			@ -606,7 +609,7 @@ function NotificationItem(props: {
 | 
			
		|||
              name={sourceUserName || ''}
 | 
			
		||||
              username={sourceUserUsername || ''}
 | 
			
		||||
              className={'mr-0 flex-shrink-0'}
 | 
			
		||||
              justFirstName={true}
 | 
			
		||||
              short={true}
 | 
			
		||||
            />
 | 
			
		||||
            <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
 | 
			
		||||
              <span className={'flex-shrink-0'}>
 | 
			
		||||
| 
						 | 
				
			
			@ -678,7 +681,7 @@ function NotificationItem(props: {
 | 
			
		|||
                    name={sourceUserName || ''}
 | 
			
		||||
                    username={sourceUserUsername || ''}
 | 
			
		||||
                    className={'relative mr-1 flex-shrink-0'}
 | 
			
		||||
                    justFirstName={true}
 | 
			
		||||
                    short={true}
 | 
			
		||||
                  />
 | 
			
		||||
                )}
 | 
			
		||||
                {getReasonForShowingNotification(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,11 +15,8 @@ export default function TagPage() {
 | 
			
		|||
      <Title text={`#${tag}`} />
 | 
			
		||||
      <ContractSearch
 | 
			
		||||
        user={user}
 | 
			
		||||
        querySortOptions={{
 | 
			
		||||
          defaultSort: 'newest',
 | 
			
		||||
          defaultFilter: 'all',
 | 
			
		||||
          shouldLoadFromStorage: true,
 | 
			
		||||
        }}
 | 
			
		||||
        defaultSort="newest"
 | 
			
		||||
        defaultFilter="all"
 | 
			
		||||
        additionalFilter={{ tag }}
 | 
			
		||||
      />
 | 
			
		||||
    </Page>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user