import React, { ReactNode, useEffect } from 'react' import Router from 'next/router' import { AdjustmentsIcon, PencilAltIcon, ArrowSmRightIcon, } from '@heroicons/react/solid' import { PlusCircleIcon, XCircleIcon } from '@heroicons/react/outline' import clsx from 'clsx' import { toast, Toaster } from 'react-hot-toast' import { Dictionary, sortBy, sum } from 'lodash' import { Page } from 'web/components/page' import { Col } from 'web/components/layout/col' import { User } from 'common/user' import { useTracking } from 'web/hooks/use-tracking' import { track } from 'web/lib/service/analytics' import { useSaveReferral } from 'web/hooks/use-save-referral' import { Sort } from 'web/components/contract-search' import { Group } from 'common/group' import { SiteLink } from 'web/components/site-link' import { usePrivateUser, useUser } from 'web/hooks/use-user' import { useMemberGroupsSubscription, useTrendingGroups, } from 'web/hooks/use-group' import { Button } from 'web/components/button' import { Row } from 'web/components/layout/row' import { ProbChangeTable } from 'web/components/contract/prob-change-table' import { groupPath, joinGroup, leaveGroup } from 'web/lib/firebase/groups' import { usePortfolioHistory } from 'web/hooks/use-portfolio-history' import { formatMoney } from 'common/util/format' import { useProbChanges } from 'web/hooks/use-prob-changes' import { calculatePortfolioProfit } from 'common/calculate-metrics' import { hasCompletedStreakToday } from 'web/components/profile/betting-streak-modal' import { ContractsGrid } from 'web/components/contract/contracts-grid' import { PillButton } from 'web/components/buttons/pill-button' import { filterDefined } from 'common/util/array' import { updateUser } from 'web/lib/firebase/users' import { isArray, keyBy } from 'lodash' import { usePrefetch } from 'web/hooks/use-prefetch' import { CPMMBinaryContract } from 'common/contract' import { useContractsByDailyScoreNotBetOn, useContractsByDailyScoreGroups, useTrendingContracts, useNewContracts, } from 'web/hooks/use-contracts' import { ProfitBadge } from 'web/components/profit-badge' import { LoadingIndicator } from 'web/components/loading-indicator' import { Input } from 'web/components/input' export default function Home() { const user = useUser() useTracking('view home') useSaveReferral() usePrefetch(user?.id) useEffect(() => { if (user === null) { // Go to landing page if not logged in. Router.push('/') } }) const { sections } = getHomeItems(user?.homeSections ?? []) useEffect(() => { if (user && !user.homeSections && sections.length > 0) { // Save initial home sections. updateUser(user.id, { homeSections: sections.map((s) => s.id) }) } }, [user, sections]) const dailyMovers = useProbChanges({ bettorId: user?.id }) const trendingContracts = useTrendingContracts(6) const newContracts = useNewContracts(6) const dailyTrendingContracts = useContractsByDailyScoreNotBetOn(user?.id, 6) const groups = useMemberGroupsSubscription(user) const trendingGroups = useTrendingGroups() const groupContracts = useContractsByDailyScoreGroups( groups?.map((g) => g.slug) ) const isLoading = !user || !dailyMovers || !trendingContracts || !newContracts || !dailyTrendingContracts return ( Router.push('/search')} /> {isLoading ? ( ) : ( <> {renderSections(sections, { score: trendingContracts, newest: newContracts, 'daily-trending': dailyTrendingContracts, 'daily-movers': dailyMovers, })} {groups && groupContracts && trendingGroups.length > 0 ? ( <> {renderGroupSections(user, groups, groupContracts)} ) : ( )} )} ) } const HOME_SECTIONS = [ { label: 'Daily trending', id: 'daily-trending' }, { label: 'Daily movers', id: 'daily-movers' }, { label: 'Trending', id: 'score' }, { label: 'New', id: 'newest' }, ] export const getHomeItems = (sections: string[]) => { // Accommodate old home sections. if (!isArray(sections)) sections = [] const itemsById = keyBy(HOME_SECTIONS, 'id') const sectionItems = filterDefined(sections.map((id) => itemsById[id])) // Add new home section items to the top. sectionItems.unshift( ...HOME_SECTIONS.filter((item) => !sectionItems.includes(item)) ) // Add unmentioned items to the end. sectionItems.push( ...HOME_SECTIONS.filter((item) => !sectionItems.includes(item)) ) return { sections: sectionItems, itemsById, } } function renderSections( sections: { id: string; label: string }[], sectionContracts: { 'daily-movers': CPMMBinaryContract[] 'daily-trending': CPMMBinaryContract[] newest: CPMMBinaryContract[] score: CPMMBinaryContract[] } ) { return ( <> {sections.map((s) => { const { id, label } = s const contracts = sectionContracts[s.id as keyof typeof sectionContracts] if (id === 'daily-movers') { return } if (id === 'daily-trending') { return ( ) } return ( ) })} ) } function renderGroupSections( user: User, groups: Group[], groupContracts: Dictionary ) { const filteredGroups = groups.filter((g) => groupContracts[g.slug]) const orderedGroups = sortBy(filteredGroups, (g) => // Sort by sum of top two daily scores. sum( sortBy(groupContracts[g.slug].map((c) => c.dailyScore)) .reverse() .slice(0, 2) ) ).reverse() const previouslySeenContracts = new Set() return ( <> {orderedGroups.map((group) => { const contracts = groupContracts[group.slug].filter( (c) => Math.abs(c.probChanges.day) >= 0.01 && !previouslySeenContracts.has(c.id) ) if (contracts.length === 0) return null contracts.forEach((c) => previouslySeenContracts.add(c.id)) return ( ) })} ) } function SectionHeader(props: { label: string href: string children?: ReactNode }) { const { label, href, children } = props return ( track('home click section header', { section: href })} > {label}{' '} {children} ) } function SearchSection(props: { label: string contracts: CPMMBinaryContract[] sort: Sort pill?: string showProbChange?: boolean }) { const { label, contracts, sort, pill, showProbChange } = props return ( ) } function GroupSection(props: { group: Group user: User contracts: CPMMBinaryContract[] }) { const { group, user, contracts } = props return ( ) } function DailyMoversSection(props: { contracts: CPMMBinaryContract[] }) { const { contracts } = props const changes = contracts.filter((c) => Math.abs(c.probChanges.day) >= 0.01) if (changes.length === 0) { return null } return ( ) } function DailyStats(props: { user: User | null | undefined className?: string }) { const { user, className } = props const metrics = usePortfolioHistory(user?.id ?? '', 'daily') ?? [] const [first, last] = [metrics[0], metrics[metrics.length - 1]] const privateUser = usePrivateUser() const streaks = privateUser?.notificationPreferences?.betting_streaks ?? [] const streaksHidden = streaks.length === 0 let profit = 0 let profitPercent = 0 if (first && last) { profit = calculatePortfolioProfit(last) - calculatePortfolioProfit(first) profitPercent = profit / first.investmentValue } return (
Daily profit
{formatMoney(profit)}{' '} {!streaksHidden && (
Streak
🔥 {user?.currentBettingStreak ?? 0} )}
) } export function TrendingGroupsSection(props: { user: User myGroups: Group[] trendingGroups: Group[] className?: string }) { const { user, myGroups, trendingGroups, className } = props const myGroupIds = new Set(myGroups.map((g) => g.id)) const groups = trendingGroups.filter((g) => !myGroupIds.has(g.id)) const count = 20 const chosenGroups = groups.slice(0, count) if (chosenGroups.length === 0) { return null } return (
Follow groups you are interested in.
{chosenGroups.map((g) => ( { if (myGroupIds.has(g.id)) leaveGroup(g, user.id) else { const homeSections = (user.homeSections ?? []) .filter((id) => id !== g.id) .concat(g.id) updateUser(user.id, { homeSections }) toast.promise(joinGroup(g, user.id), { loading: 'Following group...', success: `Followed ${g.name}`, error: "Couldn't follow group, try again?", }) track('home follow group', { group: g.slug }) } }} > ))} ) } function CustomizeButton(props: { justIcon?: boolean; className?: string }) { const { justIcon, className } = props return ( ) }