diff --git a/common/like.ts b/common/like.ts index 38b25dad..7ec14726 100644 --- a/common/like.ts +++ b/common/like.ts @@ -5,4 +5,4 @@ export type Like = { createdTime: number tipTxnId?: string // only holds most recent tip txn id } -export const LIKE_TIP_AMOUNT = 5 +export const LIKE_TIP_AMOUNT = 10 diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index 1abacb69..45c73c5e 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -5,7 +5,6 @@ import { formatMoney } from 'common/util/format' import { Col } from './layout/col' import { SiteLink } from './site-link' import { ENV_CONFIG } from 'common/envs/constants' -import { useWindowSize } from 'web/hooks/use-window-size' import { Row } from './layout/row' export function AmountInput(props: { @@ -36,9 +35,6 @@ export function AmountInput(props: { onChange(isInvalid ? undefined : amount) } - const { width } = useWindowSize() - const isMobile = (width ?? 0) < 768 - return ( <> @@ -50,7 +46,7 @@ export function AmountInput(props: { className={clsx( 'placeholder:text-greyscale-4 border-greyscale-2 rounded-md pl-9', error && 'input-error', - isMobile ? 'w-24' : '', + 'w-24 md:w-auto', inputClassName )} ref={inputRef} @@ -59,7 +55,6 @@ export function AmountInput(props: { inputMode="numeric" placeholder="0" maxLength={6} - autoFocus={!isMobile} value={amount ?? ''} disabled={disabled} onChange={(e) => onAmountChange(e.target.value)} diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index e93c0e62..beb7168a 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -47,7 +47,6 @@ import { Modal } from './layout/modal' import { Title } from './title' import toast from 'react-hot-toast' import { CheckIcon } from '@heroicons/react/solid' -import { useWindowSize } from 'web/hooks/use-window-size' export function BetPanel(props: { contract: CPMMBinaryContract | PseudoNumericContract @@ -179,12 +178,7 @@ export function BuyPanel(props: { const initialProb = getProbability(contract) const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' - const windowSize = useWindowSize() - const initialOutcome = - windowSize.width && windowSize.width >= 1280 ? 'YES' : undefined - const [outcome, setOutcome] = useState<'YES' | 'NO' | undefined>( - initialOutcome - ) + const [outcome, setOutcome] = useState<'YES' | 'NO' | undefined>() const [betAmount, setBetAmount] = useState(10) const [error, setError] = useState() const [isSubmitting, setIsSubmitting] = useState(false) diff --git a/web/components/button.tsx b/web/components/button.tsx index 51e25ea1..ecd8e1c7 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -46,7 +46,6 @@ export function Button(props: { - + ) } diff --git a/web/components/contract/tip-button.tsx b/web/components/contract/tip-button.tsx new file mode 100644 index 00000000..0315c676 --- /dev/null +++ b/web/components/contract/tip-button.tsx @@ -0,0 +1,61 @@ +import { HeartIcon } from '@heroicons/react/outline' +import { Button } from 'web/components/button' +import { formatMoney } from 'common/util/format' +import clsx from 'clsx' +import { Col } from 'web/components/layout/col' +import { Tooltip } from '../tooltip' + +export function TipButton(props: { + tipAmount: number + totalTipped: number + onClick: () => void + userTipped: boolean + isCompact?: boolean + disabled?: boolean +}) { + const { tipAmount, totalTipped, userTipped, isCompact, onClick, disabled } = + props + + return ( + + + + ) +} diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 20d124f8..b9387a03 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -177,10 +177,6 @@ export function FeedComment(props: { smallImage /> - {tips && } - {(contract.openCommentBounties ?? 0) > 0 && ( - - )} {onReplyClick && ( - - ) -} - -function UpTip(props: { onClick?: () => void; value: number }) { - const { onClick, value } = props - const IconKind = value > TIP_SIZE ? ChevronDoubleRightIcon : ChevronRightIcon - return ( - - - - ) -} diff --git a/web/hooks/use-contracts.ts b/web/hooks/use-contracts.ts index 11aae65c..7952deba 100644 --- a/web/hooks/use-contracts.ts +++ b/web/hooks/use-contracts.ts @@ -8,12 +8,14 @@ import { getUserBetContracts, getUserBetContractsQuery, listAllContracts, - trendingContractsQuery, } from 'web/lib/firebase/contracts' import { QueryClient, useQuery, useQueryClient } from 'react-query' import { MINUTE_MS, sleep } from 'common/util/time' -import { query, limit } from 'firebase/firestore' -import { dailyScoreIndex } from 'web/lib/service/algolia' +import { + dailyScoreIndex, + newIndex, + trendingIndex, +} from 'web/lib/service/algolia' import { CPMMBinaryContract } from 'common/contract' import { zipObject } from 'lodash' @@ -27,16 +29,50 @@ export const useContracts = () => { return contracts } +export const useTrendingContracts = (maxContracts: number) => { + const { data } = useQuery(['trending-contracts', maxContracts], () => + trendingIndex.search('', { + facetFilters: ['isResolved:false'], + hitsPerPage: maxContracts, + }) + ) + if (!data) return undefined + return data.hits +} + +export const useNewContracts = (maxContracts: number) => { + const { data } = useQuery(['newest-contracts', maxContracts], () => + newIndex.search('', { + facetFilters: ['isResolved:false'], + hitsPerPage: maxContracts, + }) + ) + if (!data) return undefined + return data.hits +} + +export const useContractsByDailyScoreNotBetOn = ( + userId: string | null | undefined, + maxContracts: number +) => { + const { data } = useQuery(['daily-score', userId, maxContracts], () => + dailyScoreIndex.search('', { + facetFilters: ['isResolved:false', `uniqueBettors:-${userId}`], + hitsPerPage: maxContracts, + }) + ) + if (!userId || !data) return undefined + return data.hits.filter((c) => c.dailyScore) +} + export const useContractsByDailyScoreGroups = ( groupSlugs: string[] | undefined ) => { - const facetFilters = ['isResolved:false'] - const { data } = useQuery(['daily-score', groupSlugs], () => Promise.all( (groupSlugs ?? []).map((slug) => dailyScoreIndex.search('', { - facetFilters: [...facetFilters, `groupLinks.slug:${slug}`], + facetFilters: ['isResolved:false', `groupLinks.slug:${slug}`], }) ) ) @@ -56,14 +92,6 @@ export const getCachedContracts = async () => staleTime: Infinity, }) -export const useTrendingContracts = (maxContracts: number) => { - const result = useFirestoreQueryData( - ['trending-contracts', maxContracts], - query(trendingContractsQuery, limit(maxContracts)) - ) - return result.data -} - export const useInactiveContracts = () => { const [contracts, setContracts] = useState() diff --git a/web/hooks/use-element-width.tsx b/web/hooks/use-element-width.tsx deleted file mode 100644 index 1c373839..00000000 --- a/web/hooks/use-element-width.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { RefObject, useState, useEffect } from 'react' - -// todo: consider consolidation with use-measure-size -export const useElementWidth = (ref: RefObject) => { - const [width, setWidth] = useState() - useEffect(() => { - const handleResize = () => { - setWidth(ref.current?.clientWidth) - } - handleResize() - window.addEventListener('resize', handleResize) - return () => { - window.removeEventListener('resize', handleResize) - } - }, [ref]) - return width -} diff --git a/web/hooks/use-tracking.ts b/web/hooks/use-tracking.ts index e62209c0..3d1d97ba 100644 --- a/web/hooks/use-tracking.ts +++ b/web/hooks/use-tracking.ts @@ -1,5 +1,5 @@ -import { track } from '@amplitude/analytics-browser' import { useEffect } from 'react' +import { track } from 'web/lib/service/analytics' import { inIframe } from './use-is-iframe' export const useTracking = ( @@ -10,5 +10,5 @@ export const useTracking = ( useEffect(() => { if (excludeIframe && inIframe()) return track(eventName, eventProperties) - }, []) + }, [eventName, eventProperties, excludeIframe]) } diff --git a/web/lib/service/algolia.ts b/web/lib/service/algolia.ts index 29cbd6bf..bdace399 100644 --- a/web/lib/service/algolia.ts +++ b/web/lib/service/algolia.ts @@ -14,6 +14,8 @@ export const getIndexName = (sort: string) => { return `${indexPrefix}contracts-${sort}` } +export const trendingIndex = searchClient.initIndex(getIndexName('score')) +export const newIndex = searchClient.initIndex(getIndexName('newest')) export const probChangeDescendingIndex = searchClient.initIndex( getIndexName('prob-change-day') ) diff --git a/web/pages/api/v0/market/[id]/index.ts b/web/pages/api/v0/market/[id]/index.ts index eb238dab..72e7cdbc 100644 --- a/web/pages/api/v0/market/[id]/index.ts +++ b/web/pages/api/v0/market/[id]/index.ts @@ -4,6 +4,7 @@ import { listAllComments } from 'web/lib/firebase/comments' import { getContractFromId } from 'web/lib/firebase/contracts' import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' import { FullMarket, ApiError, toFullMarket } from '../../_types' +import { marketCacheStrategy } from '../../markets' export default async function handler( req: NextApiRequest, @@ -24,6 +25,6 @@ export default async function handler( return } - res.setHeader('Cache-Control', 'max-age=0') + res.setHeader('Cache-Control', marketCacheStrategy) return res.status(200).json(toFullMarket(contract, comments, bets)) } diff --git a/web/pages/api/v0/market/[id]/lite.ts b/web/pages/api/v0/market/[id]/lite.ts index 7688caa8..6ac28db4 100644 --- a/web/pages/api/v0/market/[id]/lite.ts +++ b/web/pages/api/v0/market/[id]/lite.ts @@ -2,6 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next' import { getContractFromId } from 'web/lib/firebase/contracts' import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' import { ApiError, toLiteMarket, LiteMarket } from '../../_types' +import { marketCacheStrategy } from '../../markets' export default async function handler( req: NextApiRequest, @@ -18,6 +19,6 @@ export default async function handler( return } - res.setHeader('Cache-Control', 'max-age=0') + res.setHeader('Cache-Control', marketCacheStrategy) return res.status(200).json(toLiteMarket(contract)) } diff --git a/web/pages/api/v0/markets.ts b/web/pages/api/v0/markets.ts index 78c54772..bad6a145 100644 --- a/web/pages/api/v0/markets.ts +++ b/web/pages/api/v0/markets.ts @@ -6,6 +6,8 @@ import { toLiteMarket, ValidationError } from './_types' import { z } from 'zod' import { validate } from './_validate' +export const marketCacheStrategy = 's-maxage=15, stale-while-revalidate=45' + const queryParams = z .object({ limit: z @@ -39,7 +41,7 @@ export default async function handler( try { const contracts = await listAllContracts(limit, before) // Serve from Vercel cache, then update. see https://vercel.com/docs/concepts/functions/edge-caching - res.setHeader('Cache-Control', 's-maxage=1, stale-while-revalidate') + res.setHeader('Cache-Control', marketCacheStrategy) res.status(200).json(contracts.map(toLiteMarket)) } catch (e) { res.status(400).json({ diff --git a/web/pages/create.tsx b/web/pages/create.tsx index f5d1c605..03b90d7c 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -280,25 +280,27 @@ export function NewContract(props: { - { - if (choice === 'FREE_RESPONSE') - setMarketInfoText( - 'Users can submit their own answers to this market.' - ) - else setMarketInfoText('') - setOutcomeType(choice as outcomeType) - }} - choicesMap={{ - 'Yes / No': 'BINARY', - // 'Multiple choice': 'MULTIPLE_CHOICE', - 'Free response': 'FREE_RESPONSE', - // Numeric: 'PSEUDO_NUMERIC', - }} - isSubmitting={isSubmitting} - className={'col-span-4'} - /> + + { + if (choice === 'FREE_RESPONSE') + setMarketInfoText( + 'Users can submit their own answers to this market.' + ) + else setMarketInfoText('') + setOutcomeType(choice as outcomeType) + }} + choicesMap={{ + 'Yes / No': 'BINARY', + // 'Multiple choice': 'MULTIPLE_CHOICE', + 'Free response': 'FREE_RESPONSE', + // Numeric: 'PSEUDO_NUMERIC', + }} + isSubmitting={isSubmitting} + className={'col-span-4'} + /> + {marketInfoText && (
{marketInfoText} @@ -390,23 +392,7 @@ export function NewContract(props: { )} -
- - setVisibility(choice as visibility)} - choicesMap={{ - Public: 'public', - Unlisted: 'unlisted', - }} - isSubmitting={isSubmitting} - /> -
- - + )} + + + Display this market on homepage + + setVisibility(e.target.checked ? 'public' : 'unlisted') + } + /> + +
diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index 13480998..03bc4ce9 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -1,7 +1,7 @@ import { Bet } from 'common/bet' import { Contract } from 'common/contract' import { DOMAIN } from 'common/envs/constants' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { BetInline } from 'web/components/bet-inline' import { Button } from 'web/components/button' import { @@ -20,7 +20,6 @@ import { SiteLink } from 'web/components/site-link' import { useContractWithPreload } from 'web/hooks/use-contract' import { useMeasureSize } from 'web/hooks/use-measure-size' import { fromPropz, usePropz } from 'web/hooks/use-propz' -import { useTracking } from 'web/hooks/use-tracking' import { listAllBets } from 'web/lib/firebase/bets' import { contractPath, @@ -28,6 +27,7 @@ import { tradingAllowed, } from 'web/lib/firebase/contracts' import Custom404 from '../../404' +import { track } from 'web/lib/service/analytics' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { @@ -72,11 +72,14 @@ interface EmbedProps { export function ContractEmbed(props: EmbedProps) { const { contract } = props - useTracking('view market embed', { - slug: contract.slug, - contractId: contract.id, - creatorId: contract.creatorId, - }) + useEffect(() => { + track('view market embed', { + slug: contract.slug, + contractId: contract.id, + creatorId: contract.creatorId, + hostname: window.location.hostname, + }) + }, [contract.creatorId, contract.id, contract.slug]) // return (height < 250px) ? Card : SmolView return ( @@ -104,7 +107,7 @@ function ContractSmolView({ contract, bets }: EmbedProps) { const href = `https://${DOMAIN}${contractPath(contract)}` - const { setElem, height: graphHeight } = useMeasureSize() + const { setElem, width: graphWidth, height: graphHeight } = useMeasureSize() const [betPanelOpen, setBetPanelOpen] = useState(false) @@ -157,7 +160,14 @@ function ContractSmolView({ contract, bets }: EmbedProps) { )}
- + {graphWidth != null && graphHeight != null && ( + + )}
) diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx index 2ddc3026..4ad7f97b 100644 --- a/web/pages/home/index.tsx +++ b/web/pages/home/index.tsx @@ -12,7 +12,6 @@ import { Dictionary, sortBy, sum } from 'lodash' import { Page } from 'web/components/page' import { Col } from 'web/components/layout/col' -import { ContractSearch, SORTS } from 'web/components/contract-search' import { User } from 'common/user' import { useTracking } from 'web/hooks/use-tracking' import { track } from 'web/lib/service/analytics' @@ -43,7 +42,12 @@ import { isArray, keyBy } from 'lodash' import { usePrefetch } from 'web/hooks/use-prefetch' import { Title } from 'web/components/title' import { CPMMBinaryContract } from 'common/contract' -import { useContractsByDailyScoreGroups } from 'web/hooks/use-contracts' +import { + useContractsByDailyScoreNotBetOn, + useContractsByDailyScoreGroups, + useTrendingContracts, + useNewContracts, +} from 'web/hooks/use-contracts' import { ProfitBadge } from 'web/components/profit-badge' import { LoadingIndicator } from 'web/components/loading-indicator' @@ -71,12 +75,18 @@ export default function Home() { } }, [user, sections]) - const groups = useMemberGroupsSubscription(user) + const trendingContracts = useTrendingContracts(6) + const newContracts = useNewContracts(6) + const dailyTrendingContracts = useContractsByDailyScoreNotBetOn(user?.id, 6) + const groups = useMemberGroupsSubscription(user) const groupContracts = useContractsByDailyScoreGroups( groups?.map((g) => g.slug) ) + const isLoading = + !user || !trendingContracts || !newContracts || !dailyTrendingContracts + return ( @@ -90,11 +100,15 @@ export default function Home() { - {!user ? ( + {isLoading ? ( ) : ( <> - {sections.map((section) => renderSection(section, user))} + {renderSections(user, sections, { + score: trendingContracts, + newest: newContracts, + 'daily-trending': dailyTrendingContracts, + })} @@ -118,8 +132,8 @@ export default function Home() { } const HOME_SECTIONS = [ - { label: 'Daily movers', id: 'daily-movers' }, { label: 'Daily trending', id: 'daily-trending' }, + { label: 'Daily movers', id: 'daily-movers' }, { label: 'Trending', id: 'score' }, { label: 'New', id: 'newest' }, ] @@ -128,11 +142,7 @@ export const getHomeItems = (sections: string[]) => { // Accommodate old home sections. if (!isArray(sections)) sections = [] - const items: { id: string; label: string; group?: Group }[] = [ - ...HOME_SECTIONS, - ] - const itemsById = keyBy(items, 'id') - + const itemsById = keyBy(HOME_SECTIONS, 'id') const sectionItems = filterDefined(sections.map((id) => itemsById[id])) // Add new home section items to the top. @@ -140,7 +150,9 @@ export const getHomeItems = (sections: string[]) => { ...HOME_SECTIONS.filter((item) => !sectionItems.includes(item)) ) // Add unmentioned items to the end. - sectionItems.push(...items.filter((item) => !sectionItems.includes(item))) + sectionItems.push( + ...HOME_SECTIONS.filter((item) => !sectionItems.includes(item)) + ) return { sections: sectionItems, @@ -148,28 +160,46 @@ export const getHomeItems = (sections: string[]) => { } } -function renderSection(section: { id: string; label: string }, user: User) { - const { id, label } = section - if (id === 'daily-movers') { - return +function renderSections( + user: User, + sections: { id: string; label: string }[], + sectionContracts: { + 'daily-trending': CPMMBinaryContract[] + newest: CPMMBinaryContract[] + score: CPMMBinaryContract[] } - if (id === 'daily-trending') - return ( - - ) - const sort = SORTS.find((sort) => sort.value === id) - if (sort) - return ( - - ) - - return null +) { + return ( + <> + {sections.map((s) => { + const { id, label } = s + if (id === 'daily-movers') { + return + } + if (id === 'daily-trending') { + return ( + + ) + } + const contracts = + sectionContracts[s.id as keyof typeof sectionContracts] + return ( + + ) + })} + + ) } function renderGroupSections( @@ -237,13 +267,14 @@ function SectionHeader(props: { ) } -function SearchSection(props: { +function ContractsSection(props: { label: string - user: User + contracts: CPMMBinaryContract[] sort: Sort pill?: string + showProbChange?: boolean }) { - const { label, user, sort, pill } = props + const { label, contracts, sort, pill, showProbChange } = props return ( @@ -251,14 +282,7 @@ function SearchSection(props: { label={label} href={`/search?s=${sort}${pill ? `&p=${pill}` : ''}`} /> - + ) } diff --git a/web/pages/labs/index.tsx b/web/pages/labs/index.tsx new file mode 100644 index 00000000..6ea861a3 --- /dev/null +++ b/web/pages/labs/index.tsx @@ -0,0 +1,43 @@ +import Masonry from 'react-masonry-css' +import { Page } from 'web/components/page' +import { SiteLink } from 'web/components/site-link' +import { Title } from 'web/components/title' + +export default function LabsPage() { + return ( + + + + <Masonry + breakpointCols={{ default: 2, 768: 1 }} + className="-ml-4 flex w-auto" + columnClassName="pl-4 bg-clip-padding" + > + <LabCard + title="Dating docs" + description="Browse dating docs or create your own" + href="/date-docs" + /> + </Masonry> + </Page> + ) +} + +const LabCard = (props: { + title: string + description: string + href: string +}) => { + const { title, description, href } = props + return ( + <SiteLink + href={href} + className="group flex h-full w-full flex-col rounded-lg bg-white p-4 shadow-md transition-shadow duration-200 hover:no-underline hover:shadow-lg" + > + <h3 className="text-lg font-semibold group-hover:underline group-hover:decoration-indigo-400 group-hover:decoration-2"> + {title} + </h3> + <p className="mt-2 text-gray-600">{description}</p> + </SiteLink> + ) +} diff --git a/web/posts/post-comments.tsx b/web/posts/post-comments.tsx index f1d50a29..74fbb300 100644 --- a/web/posts/post-comments.tsx +++ b/web/posts/post-comments.tsx @@ -154,7 +154,6 @@ export function PostComment(props: { smallImage /> <Row className="mt-2 items-center gap-6 text-xs text-gray-500"> - <Tipper comment={comment} tips={tips ?? {}} /> {onReplyClick && ( <button className="font-bold hover:underline" @@ -163,6 +162,7 @@ export function PostComment(props: { Reply </button> )} + <Tipper comment={comment} tips={tips ?? {}} /> </Row> </div> </Row>