Updates to experimental home (#858)
* Line clamp question in prob change table * Tweaks * Expand option for daily movers * Snap scrolling for carousel * Add arrows to section headers * Remove carousel from experimental/home * React querify fetching your groups * Edit home is its own page * Add daily profit and balance * Merge branch 'main' into new-home * Make experimental search by your followed groups/creators * Just submit, allow xs on pills * Weigh in * Use next/future/image component to optimize avatar images * Inga/challenge icon (#857) * changed challenge icon to custom icon * fixed tip button alignment * weighing in and trading "weigh in" for "trade" Co-authored-by: Ian Philips <iansphilips@gmail.com> Co-authored-by: Austin Chen <akrolsmir@gmail.com> Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com> Co-authored-by: mantikoros <sgrugett@gmail.com>
This commit is contained in:
parent
edbebb7e67
commit
54c227cf6c
|
@ -116,12 +116,12 @@ const calculateProfitForPeriod = (
|
||||||
return currentProfit
|
return currentProfit
|
||||||
}
|
}
|
||||||
|
|
||||||
const startingProfit = calculateTotalProfit(startingPortfolio)
|
const startingProfit = calculatePortfolioProfit(startingPortfolio)
|
||||||
|
|
||||||
return currentProfit - startingProfit
|
return currentProfit - startingProfit
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateTotalProfit = (portfolio: PortfolioMetrics) => {
|
export const calculatePortfolioProfit = (portfolio: PortfolioMetrics) => {
|
||||||
return portfolio.investmentValue + portfolio.balance - portfolio.totalDeposits
|
return portfolio.investmentValue + portfolio.balance - portfolio.totalDeposits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ export const calculateNewProfit = (
|
||||||
portfolioHistory: PortfolioMetrics[],
|
portfolioHistory: PortfolioMetrics[],
|
||||||
newPortfolio: PortfolioMetrics
|
newPortfolio: PortfolioMetrics
|
||||||
) => {
|
) => {
|
||||||
const allTimeProfit = calculateTotalProfit(newPortfolio)
|
const allTimeProfit = calculatePortfolioProfit(newPortfolio)
|
||||||
const descendingPortfolio = sortBy(
|
const descendingPortfolio = sortBy(
|
||||||
portfolioHistory,
|
portfolioHistory,
|
||||||
(p) => p.timestamp
|
(p) => p.timestamp
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { User } from 'common/user'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
|
|
||||||
export function ArrangeHome(props: {
|
export function ArrangeHome(props: {
|
||||||
user: User | null
|
user: User | null | undefined
|
||||||
homeSections: { visible: string[]; hidden: string[] }
|
homeSections: { visible: string[]; hidden: string[] }
|
||||||
setHomeSections: (homeSections: {
|
setHomeSections: (homeSections: {
|
||||||
visible: string[]
|
visible: string[]
|
||||||
|
@ -30,7 +30,6 @@ export function ArrangeHome(props: {
|
||||||
return (
|
return (
|
||||||
<DragDropContext
|
<DragDropContext
|
||||||
onDragEnd={(e) => {
|
onDragEnd={(e) => {
|
||||||
console.log('drag end', e)
|
|
||||||
const { destination, source, draggableId } = e
|
const { destination, source, draggableId } = e
|
||||||
if (!destination) return
|
if (!destination) return
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ export function Carousel(props: {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('relative', className)}>
|
<div className={clsx('relative', className)}>
|
||||||
<Row
|
<Row
|
||||||
className="scrollbar-hide w-full gap-4 overflow-x-auto scroll-smooth"
|
className="scrollbar-hide w-full snap-x gap-4 overflow-x-auto scroll-smooth"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
>
|
>
|
||||||
|
|
|
@ -69,6 +69,7 @@ type AdditionalFilter = {
|
||||||
excludeContractIds?: string[]
|
excludeContractIds?: string[]
|
||||||
groupSlug?: string
|
groupSlug?: string
|
||||||
yourBets?: boolean
|
yourBets?: boolean
|
||||||
|
followed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContractSearch(props: {
|
export function ContractSearch(props: {
|
||||||
|
@ -88,6 +89,7 @@ export function ContractSearch(props: {
|
||||||
useQueryUrlParam?: boolean
|
useQueryUrlParam?: boolean
|
||||||
isWholePage?: boolean
|
isWholePage?: boolean
|
||||||
noControls?: boolean
|
noControls?: boolean
|
||||||
|
maxResults?: number
|
||||||
renderContracts?: (
|
renderContracts?: (
|
||||||
contracts: Contract[] | undefined,
|
contracts: Contract[] | undefined,
|
||||||
loadMore: () => void
|
loadMore: () => void
|
||||||
|
@ -107,6 +109,7 @@ export function ContractSearch(props: {
|
||||||
useQueryUrlParam,
|
useQueryUrlParam,
|
||||||
isWholePage,
|
isWholePage,
|
||||||
noControls,
|
noControls,
|
||||||
|
maxResults,
|
||||||
renderContracts,
|
renderContracts,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
@ -189,7 +192,8 @@ export function ContractSearch(props: {
|
||||||
const contracts = state.pages
|
const contracts = state.pages
|
||||||
.flat()
|
.flat()
|
||||||
.filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id))
|
.filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id))
|
||||||
const renderedContracts = state.pages.length === 0 ? undefined : contracts
|
const renderedContracts =
|
||||||
|
state.pages.length === 0 ? undefined : contracts.slice(0, maxResults)
|
||||||
|
|
||||||
if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
|
if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
|
||||||
return <ContractSearchFirestore additionalFilter={additionalFilter} />
|
return <ContractSearchFirestore additionalFilter={additionalFilter} />
|
||||||
|
@ -292,6 +296,19 @@ function ContractSearchControls(props: {
|
||||||
const pillGroups: { name: string; slug: string }[] =
|
const pillGroups: { name: string; slug: string }[] =
|
||||||
memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS
|
memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS
|
||||||
|
|
||||||
|
const personalFilters = user
|
||||||
|
? [
|
||||||
|
// Show contracts in groups that the user is a member of.
|
||||||
|
memberGroupSlugs
|
||||||
|
.map((slug) => `groupLinks.slug:${slug}`)
|
||||||
|
// Or, show contracts created by users the user follows
|
||||||
|
.concat(follows?.map((followId) => `creatorId:${followId}`) ?? []),
|
||||||
|
|
||||||
|
// Subtract contracts you bet on, to show new ones.
|
||||||
|
`uniqueBettorIds:-${user.id}`,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
|
||||||
const additionalFilters = [
|
const additionalFilters = [
|
||||||
additionalFilter?.creatorId
|
additionalFilter?.creatorId
|
||||||
? `creatorId:${additionalFilter.creatorId}`
|
? `creatorId:${additionalFilter.creatorId}`
|
||||||
|
@ -304,6 +321,7 @@ function ContractSearchControls(props: {
|
||||||
? // Show contracts bet on by the user
|
? // Show contracts bet on by the user
|
||||||
`uniqueBettorIds:${user.id}`
|
`uniqueBettorIds:${user.id}`
|
||||||
: '',
|
: '',
|
||||||
|
...(additionalFilter?.followed ? personalFilters : []),
|
||||||
]
|
]
|
||||||
const facetFilters = query
|
const facetFilters = query
|
||||||
? additionalFilters
|
? additionalFilters
|
||||||
|
@ -320,17 +338,7 @@ function ContractSearchControls(props: {
|
||||||
state.pillFilter !== 'your-bets'
|
state.pillFilter !== 'your-bets'
|
||||||
? `groupLinks.slug:${state.pillFilter}`
|
? `groupLinks.slug:${state.pillFilter}`
|
||||||
: '',
|
: '',
|
||||||
state.pillFilter === 'personal'
|
...(state.pillFilter === 'personal' ? personalFilters : []),
|
||||||
? // Show contracts in groups that the user is a member of
|
|
||||||
memberGroupSlugs
|
|
||||||
.map((slug) => `groupLinks.slug:${slug}`)
|
|
||||||
// Show contracts created by users the user follows
|
|
||||||
.concat(follows?.map((followId) => `creatorId:${followId}`) ?? [])
|
|
||||||
: '',
|
|
||||||
// Subtract contracts you bet on from For you.
|
|
||||||
state.pillFilter === 'personal' && user
|
|
||||||
? `uniqueBettorIds:-${user.id}`
|
|
||||||
: '',
|
|
||||||
state.pillFilter === 'your-bets' && user
|
state.pillFilter === 'your-bets' && user
|
||||||
? // Show contracts bet on by the user
|
? // Show contracts bet on by the user
|
||||||
`uniqueBettorIds:${user.id}`
|
`uniqueBettorIds:${user.id}`
|
||||||
|
|
|
@ -3,52 +3,74 @@ import { contractPath } from 'web/lib/firebase/contracts'
|
||||||
import { CPMMContract } from 'common/contract'
|
import { CPMMContract } from 'common/contract'
|
||||||
import { formatPercent } from 'common/util/format'
|
import { formatPercent } from 'common/util/format'
|
||||||
import { useProbChanges } from 'web/hooks/use-prob-changes'
|
import { useProbChanges } from 'web/hooks/use-prob-changes'
|
||||||
import { SiteLink } from '../site-link'
|
import { linkClass, SiteLink } from '../site-link'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
export function ProbChangeTable(props: { userId: string | undefined }) {
|
export function ProbChangeTable(props: { userId: string | undefined }) {
|
||||||
const { userId } = props
|
const { userId } = props
|
||||||
|
|
||||||
const changes = useProbChanges(userId ?? '')
|
const changes = useProbChanges(userId ?? '')
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
if (!changes) {
|
if (!changes) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { positiveChanges, negativeChanges } = changes
|
const count = expanded ? 16 : 4
|
||||||
|
|
||||||
const count = 3
|
const { positiveChanges, negativeChanges } = changes
|
||||||
|
const filteredPositiveChanges = positiveChanges.slice(0, count / 2)
|
||||||
|
const filteredNegativeChanges = negativeChanges.slice(0, count / 2)
|
||||||
|
const filteredChanges = [
|
||||||
|
...filteredPositiveChanges,
|
||||||
|
...filteredNegativeChanges,
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="w-full flex-wrap divide-x-2 rounded bg-white shadow-md">
|
<Col>
|
||||||
<Col className="min-w-[300px] flex-1 divide-y">
|
<Col className="mb-4 w-full divide-x-2 divide-y rounded-lg bg-white shadow-md md:flex-row md:divide-y-0">
|
||||||
{positiveChanges.slice(0, count).map((contract) => (
|
<Col className="flex-1 divide-y">
|
||||||
<Row className="hover:bg-gray-100">
|
{filteredChanges.slice(0, count / 2).map((contract) => (
|
||||||
<ProbChange className="p-4 text-right" contract={contract} />
|
<Row className="items-center hover:bg-gray-100">
|
||||||
|
<ProbChange
|
||||||
|
className="p-4 text-right text-xl"
|
||||||
|
contract={contract}
|
||||||
|
/>
|
||||||
<SiteLink
|
<SiteLink
|
||||||
className="p-4 font-semibold text-indigo-700"
|
className="p-4 pl-2 font-semibold text-indigo-700"
|
||||||
href={contractPath(contract)}
|
href={contractPath(contract)}
|
||||||
>
|
>
|
||||||
{contract.question}
|
<span className="line-clamp-2">{contract.question}</span>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="justify-content-stretch min-w-[300px] flex-1 divide-y">
|
<Col className="flex-1 divide-y">
|
||||||
{negativeChanges.slice(0, count).map((contract) => (
|
{filteredChanges.slice(count / 2).map((contract) => (
|
||||||
<Row className="hover:bg-gray-100">
|
<Row className="items-center hover:bg-gray-100">
|
||||||
<ProbChange className="p-4 text-right" contract={contract} />
|
<ProbChange
|
||||||
|
className="p-4 text-right text-xl"
|
||||||
|
contract={contract}
|
||||||
|
/>
|
||||||
<SiteLink
|
<SiteLink
|
||||||
className="p-4 font-semibold text-indigo-700"
|
className="p-4 pl-2 font-semibold text-indigo-700"
|
||||||
href={contractPath(contract)}
|
href={contractPath(contract)}
|
||||||
>
|
>
|
||||||
{contract.question}
|
<span className="line-clamp-2">{contract.question}</span>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Col>
|
||||||
|
<div
|
||||||
|
className={clsx(linkClass, 'cursor-pointer self-end')}
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
>
|
||||||
|
{expanded ? 'Show less' : 'Show more'}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,9 +85,9 @@ export function ProbChange(props: {
|
||||||
|
|
||||||
const color =
|
const color =
|
||||||
change > 0
|
change > 0
|
||||||
? 'text-green-600'
|
? 'text-green-500'
|
||||||
: change < 0
|
: change < 0
|
||||||
? 'text-red-600'
|
? 'text-red-500'
|
||||||
: 'text-gray-600'
|
: 'text-gray-600'
|
||||||
|
|
||||||
const str =
|
const str =
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { Col } from 'web/components/layout/col'
|
||||||
|
|
||||||
export function DoubleCarousel(props: {
|
export function DoubleCarousel(props: {
|
||||||
contracts: Contract[]
|
contracts: Contract[]
|
||||||
seeMoreUrl?: string
|
|
||||||
showTime?: ShowTime
|
showTime?: ShowTime
|
||||||
loadMore?: () => void
|
loadMore?: () => void
|
||||||
}) {
|
}) {
|
||||||
|
@ -19,7 +18,7 @@ export function DoubleCarousel(props: {
|
||||||
? range(0, Math.floor(contracts.length / 2)).map((col) => {
|
? range(0, Math.floor(contracts.length / 2)).map((col) => {
|
||||||
const i = col * 2
|
const i = col * 2
|
||||||
return (
|
return (
|
||||||
<Col key={contracts[i].id}>
|
<Col className="snap-start scroll-m-4" key={contracts[i].id}>
|
||||||
<ContractCard
|
<ContractCard
|
||||||
contract={contracts[i]}
|
contract={contracts[i]}
|
||||||
className="mb-2 w-96 shrink-0"
|
className="mb-2 w-96 shrink-0"
|
||||||
|
|
|
@ -2,13 +2,13 @@ import { useEffect, useState } from 'react'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import {
|
import {
|
||||||
|
getMemberGroups,
|
||||||
GroupMemberDoc,
|
GroupMemberDoc,
|
||||||
groupMembers,
|
groupMembers,
|
||||||
listenForGroup,
|
listenForGroup,
|
||||||
listenForGroupContractDocs,
|
listenForGroupContractDocs,
|
||||||
listenForGroups,
|
listenForGroups,
|
||||||
listenForMemberGroupIds,
|
listenForMemberGroupIds,
|
||||||
listenForMemberGroups,
|
|
||||||
listenForOpenGroups,
|
listenForOpenGroups,
|
||||||
listGroups,
|
listGroups,
|
||||||
} from 'web/lib/firebase/groups'
|
} from 'web/lib/firebase/groups'
|
||||||
|
@ -17,6 +17,7 @@ import { filterDefined } from 'common/util/array'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { uniq } from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
import { listenForValues } from 'web/lib/firebase/utils'
|
import { listenForValues } from 'web/lib/firebase/utils'
|
||||||
|
import { useQuery } from 'react-query'
|
||||||
|
|
||||||
export const useGroup = (groupId: string | undefined) => {
|
export const useGroup = (groupId: string | undefined) => {
|
||||||
const [group, setGroup] = useState<Group | null | undefined>()
|
const [group, setGroup] = useState<Group | null | undefined>()
|
||||||
|
@ -49,12 +50,10 @@ export const useOpenGroups = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMemberGroups = (userId: string | null | undefined) => {
|
export const useMemberGroups = (userId: string | null | undefined) => {
|
||||||
const [memberGroups, setMemberGroups] = useState<Group[] | undefined>()
|
const result = useQuery(['member-groups', userId ?? ''], () =>
|
||||||
useEffect(() => {
|
getMemberGroups(userId ?? '')
|
||||||
if (userId)
|
)
|
||||||
return listenForMemberGroups(userId, (groups) => setMemberGroups(groups))
|
return result.data
|
||||||
}, [userId])
|
|
||||||
return memberGroups
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: We cache member group ids in localstorage to speed up the initial load
|
// Note: We cache member group ids in localstorage to speed up the initial load
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const groupMembers = (groupId: string) =>
|
||||||
export const groupContracts = (groupId: string) =>
|
export const groupContracts = (groupId: string) =>
|
||||||
collection(groups, groupId, 'groupContracts')
|
collection(groups, groupId, 'groupContracts')
|
||||||
const openGroupsQuery = query(groups, where('anyoneCanJoin', '==', true))
|
const openGroupsQuery = query(groups, where('anyoneCanJoin', '==', true))
|
||||||
const memberGroupsQuery = (userId: string) =>
|
export const memberGroupsQuery = (userId: string) =>
|
||||||
query(collectionGroup(db, 'groupMembers'), where('userId', '==', userId))
|
query(collectionGroup(db, 'groupMembers'), where('userId', '==', userId))
|
||||||
|
|
||||||
export function groupPath(
|
export function groupPath(
|
||||||
|
@ -113,6 +113,15 @@ export function listenForGroup(
|
||||||
return listenForValue(doc(groups, groupId), setGroup)
|
return listenForValue(doc(groups, groupId), setGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMemberGroups(userId: string) {
|
||||||
|
const snapshot = await getDocs(memberGroupsQuery(userId))
|
||||||
|
const groupIds = filterDefined(
|
||||||
|
snapshot.docs.map((doc) => doc.ref.parent.parent?.id)
|
||||||
|
)
|
||||||
|
const groups = await Promise.all(groupIds.map(getGroup))
|
||||||
|
return filterDefined(groups)
|
||||||
|
}
|
||||||
|
|
||||||
export function listenForMemberGroupIds(
|
export function listenForMemberGroupIds(
|
||||||
userId: string,
|
userId: string,
|
||||||
setGroupIds: (groupIds: string[]) => void
|
setGroupIds: (groupIds: string[]) => void
|
||||||
|
|
60
web/pages/experimental/home/edit.tsx
Normal file
60
web/pages/experimental/home/edit.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ArrangeHome } from 'web/components/arrange-home'
|
||||||
|
import { Button } from 'web/components/button'
|
||||||
|
import { Col } from 'web/components/layout/col'
|
||||||
|
import { Row } from 'web/components/layout/row'
|
||||||
|
import { Page } from 'web/components/page'
|
||||||
|
import { SiteLink } from 'web/components/site-link'
|
||||||
|
import { Title } from 'web/components/title'
|
||||||
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { updateUser } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
useTracking('edit home')
|
||||||
|
|
||||||
|
const [homeSections, setHomeSections] = useState(
|
||||||
|
user?.homeSections ?? { visible: [], hidden: [] }
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateHomeSections = (newHomeSections: {
|
||||||
|
visible: string[]
|
||||||
|
hidden: string[]
|
||||||
|
}) => {
|
||||||
|
if (!user) return
|
||||||
|
updateUser(user.id, { homeSections: newHomeSections })
|
||||||
|
setHomeSections(newHomeSections)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<Col className="pm:mx-10 gap-4 px-4 pb-12">
|
||||||
|
<Row className={'w-full items-center justify-between'}>
|
||||||
|
<Title text="Edit your home page" />
|
||||||
|
<DoneButton />
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<ArrangeHome
|
||||||
|
user={user}
|
||||||
|
homeSections={homeSections}
|
||||||
|
setHomeSections={updateHomeSections}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DoneButton(props: { className?: string }) {
|
||||||
|
const { className } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SiteLink href="/experimental/home">
|
||||||
|
<Button size="lg" color="blue" className={clsx(className, 'flex')}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</SiteLink>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,40 +1,36 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import Router from 'next/router'
|
import Router from 'next/router'
|
||||||
import { PencilIcon, PlusSmIcon } from '@heroicons/react/solid'
|
import {
|
||||||
|
PencilIcon,
|
||||||
|
PlusSmIcon,
|
||||||
|
ArrowSmRightIcon,
|
||||||
|
} from '@heroicons/react/solid'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { ContractSearch, SORTS } from 'web/components/contract-search'
|
import { ContractSearch, SORTS } from 'web/components/contract-search'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { getUserAndPrivateUser, updateUser } from 'web/lib/firebase/users'
|
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { authenticateOnServer } from 'web/lib/firebase/server-auth'
|
|
||||||
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
import { GetServerSideProps } from 'next'
|
|
||||||
import { Sort } from 'web/components/contract-search'
|
import { Sort } from 'web/components/contract-search'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
|
||||||
import { GroupLinkItem } from '../../groups'
|
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { useMemberGroups } from 'web/hooks/use-group'
|
import { useMemberGroups } from 'web/hooks/use-group'
|
||||||
import { DoubleCarousel } from '../../../components/double-carousel'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Button } from 'web/components/button'
|
import { Button } from 'web/components/button'
|
||||||
import { ArrangeHome, getHomeItems } from '../../../components/arrange-home'
|
import { getHomeItems } from '../../../components/arrange-home'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { ProbChangeTable } from 'web/components/contract/prob-change-table'
|
import { ProbChangeTable } from 'web/components/contract/prob-change-table'
|
||||||
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
|
import { usePortfolioHistory } from 'web/hooks/use-portfolio-history'
|
||||||
|
import { calculatePortfolioProfit } from 'common/calculate-metrics'
|
||||||
|
import { formatMoney } from 'common/util/format'
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
const Home = () => {
|
||||||
const creds = await authenticateOnServer(ctx)
|
const user = useUser()
|
||||||
const auth = creds ? await getUserAndPrivateUser(creds.uid) : null
|
|
||||||
return { props: { auth } }
|
|
||||||
}
|
|
||||||
|
|
||||||
const Home = (props: { auth: { user: User } | null }) => {
|
|
||||||
const user = useUser() ?? props.auth?.user ?? null
|
|
||||||
|
|
||||||
useTracking('view home')
|
useTracking('view home')
|
||||||
|
|
||||||
|
@ -42,41 +38,22 @@ const Home = (props: { auth: { user: User } | null }) => {
|
||||||
|
|
||||||
const groups = useMemberGroups(user?.id) ?? []
|
const groups = useMemberGroups(user?.id) ?? []
|
||||||
|
|
||||||
const [homeSections, setHomeSections] = useState(
|
const [homeSections] = useState(
|
||||||
user?.homeSections ?? { visible: [], hidden: [] }
|
user?.homeSections ?? { visible: [], hidden: [] }
|
||||||
)
|
)
|
||||||
const { visibleItems } = getHomeItems(groups, homeSections)
|
const { visibleItems } = getHomeItems(groups, homeSections)
|
||||||
|
|
||||||
const updateHomeSections = (newHomeSections: {
|
|
||||||
visible: string[]
|
|
||||||
hidden: string[]
|
|
||||||
}) => {
|
|
||||||
if (!user) return
|
|
||||||
updateUser(user.id, { homeSections: newHomeSections })
|
|
||||||
setHomeSections(newHomeSections)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Col className="pm:mx-10 gap-4 px-4 pb-12 xl:w-[125%]">
|
<Col className="pm:mx-10 gap-4 px-4 pb-12">
|
||||||
<Row className={'w-full items-center justify-between'}>
|
<Row className={'w-full items-center justify-between'}>
|
||||||
<Title text={isEditing ? 'Edit your home page' : 'Home'} />
|
<Title className="!mb-0" text="Home" />
|
||||||
|
|
||||||
<EditDoneButton isEditing={isEditing} setIsEditing={setIsEditing} />
|
<EditButton />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{isEditing ? (
|
<DailyProfitAndBalance userId={user?.id} />
|
||||||
<>
|
|
||||||
<ArrangeHome
|
|
||||||
user={user}
|
|
||||||
homeSections={homeSections}
|
|
||||||
setHomeSections={updateHomeSections}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="text-xl text-gray-800">Daily movers</div>
|
<div className="text-xl text-gray-800">Daily movers</div>
|
||||||
<ProbChangeTable userId={user?.id} />
|
<ProbChangeTable userId={user?.id} />
|
||||||
|
|
||||||
|
@ -87,7 +64,7 @@ const Home = (props: { auth: { user: User } | null }) => {
|
||||||
<SearchSection
|
<SearchSection
|
||||||
key={id}
|
key={id}
|
||||||
label={'Your trades'}
|
label={'Your trades'}
|
||||||
sort={'prob-change-day'}
|
sort={'newest'}
|
||||||
user={user}
|
user={user}
|
||||||
yourBets
|
yourBets
|
||||||
/>
|
/>
|
||||||
|
@ -105,13 +82,10 @@ const Home = (props: { auth: { user: User } | null }) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const group = groups.find((g) => g.id === id)
|
const group = groups.find((g) => g.id === id)
|
||||||
if (group)
|
if (group) return <GroupSection key={id} group={group} user={user} />
|
||||||
return <GroupSection key={id} group={group} user={user} />
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
})}
|
})}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -129,7 +103,7 @@ const Home = (props: { auth: { user: User } | null }) => {
|
||||||
|
|
||||||
function SearchSection(props: {
|
function SearchSection(props: {
|
||||||
label: string
|
label: string
|
||||||
user: User | null
|
user: User | null | undefined
|
||||||
sort: Sort
|
sort: Sort
|
||||||
yourBets?: boolean
|
yourBets?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
@ -139,88 +113,91 @@ function SearchSection(props: {
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<SiteLink className="mb-2 text-xl" href={href}>
|
<SiteLink className="mb-2 text-xl" href={href}>
|
||||||
{label}
|
{label}{' '}
|
||||||
|
<ArrowSmRightIcon
|
||||||
|
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
<ContractSearch
|
<ContractSearch
|
||||||
user={user}
|
user={user}
|
||||||
defaultSort={sort}
|
defaultSort={sort}
|
||||||
additionalFilter={yourBets ? { yourBets: true } : undefined}
|
additionalFilter={yourBets ? { yourBets: true } : { followed: true }}
|
||||||
noControls
|
noControls
|
||||||
// persistPrefix={`experimental-home-${sort}`}
|
maxResults={6}
|
||||||
renderContracts={(contracts, loadMore) =>
|
persistPrefix={`experimental-home-${sort}`}
|
||||||
contracts ? (
|
|
||||||
<DoubleCarousel
|
|
||||||
contracts={contracts}
|
|
||||||
seeMoreUrl={href}
|
|
||||||
showTime={
|
|
||||||
sort === 'close-date' || sort === 'resolve-date'
|
|
||||||
? sort
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
loadMore={loadMore}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<LoadingIndicator />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupSection(props: { group: Group; user: User | null }) {
|
function GroupSection(props: { group: Group; user: User | null | undefined }) {
|
||||||
const { group, user } = props
|
const { group, user } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<GroupLinkItem className="mb-2 text-xl" group={group} />
|
<SiteLink className="mb-2 text-xl" href={groupPath(group.slug)}>
|
||||||
|
{group.name}{' '}
|
||||||
|
<ArrowSmRightIcon
|
||||||
|
className="mb-0.5 inline h-6 w-6 text-gray-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</SiteLink>
|
||||||
<ContractSearch
|
<ContractSearch
|
||||||
user={user}
|
user={user}
|
||||||
defaultSort={'score'}
|
defaultSort={'score'}
|
||||||
additionalFilter={{ groupSlug: group.slug }}
|
additionalFilter={{ groupSlug: group.slug }}
|
||||||
noControls
|
noControls
|
||||||
// persistPrefix={`experimental-home-${group.slug}`}
|
maxResults={6}
|
||||||
renderContracts={(contracts, loadMore) =>
|
persistPrefix={`experimental-home-${group.slug}`}
|
||||||
contracts ? (
|
|
||||||
contracts.length == 0 ? (
|
|
||||||
<div className="m-2 text-gray-500">No open markets</div>
|
|
||||||
) : (
|
|
||||||
<DoubleCarousel
|
|
||||||
contracts={contracts}
|
|
||||||
seeMoreUrl={`/group/${group.slug}`}
|
|
||||||
loadMore={loadMore}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<LoadingIndicator />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditDoneButton(props: {
|
function EditButton(props: { className?: string }) {
|
||||||
isEditing: boolean
|
const { className } = props
|
||||||
setIsEditing: (isEditing: boolean) => void
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const { isEditing, setIsEditing, className } = props
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<SiteLink href="/experimental/home/edit">
|
||||||
size="lg"
|
<Button size="lg" color="gray-white" className={clsx(className, 'flex')}>
|
||||||
color={isEditing ? 'blue' : 'gray-white'}
|
<PencilIcon className={clsx('mr-2 h-[24px] w-5')} aria-hidden="true" />{' '}
|
||||||
className={clsx(className, 'flex')}
|
Edit
|
||||||
onClick={() => {
|
|
||||||
setIsEditing(!isEditing)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!isEditing && (
|
|
||||||
<PencilIcon className={clsx('mr-2 h-[24px] w-5')} aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
{isEditing ? 'Done' : 'Edit'}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
</SiteLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DailyProfitAndBalance(props: {
|
||||||
|
userId: string | null | undefined
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { userId, className } = props
|
||||||
|
const metrics = usePortfolioHistory(userId ?? '', 'daily') ?? []
|
||||||
|
const [first, last] = [metrics[0], metrics[metrics.length - 1]]
|
||||||
|
|
||||||
|
if (first === undefined || last === undefined) return null
|
||||||
|
|
||||||
|
const profit =
|
||||||
|
calculatePortfolioProfit(last) - calculatePortfolioProfit(first)
|
||||||
|
|
||||||
|
const balanceChange = last.balance - first.balance
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(className, 'text-lg')}>
|
||||||
|
<span className={clsx(profit >= 0 ? 'text-green-500' : 'text-red-500')}>
|
||||||
|
{profit >= 0 ? '+' : '-'}
|
||||||
|
{formatMoney(profit)}
|
||||||
|
</span>{' '}
|
||||||
|
profit and{' '}
|
||||||
|
<span
|
||||||
|
className={clsx(balanceChange >= 0 ? 'text-green-500' : 'text-red-500')}
|
||||||
|
>
|
||||||
|
{balanceChange >= 0 ? '+' : '-'}
|
||||||
|
{formatMoney(balanceChange)}
|
||||||
|
</span>{' '}
|
||||||
|
balance today
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -237,7 +237,7 @@ const MarketCarousel = (props: { slug: string }) => {
|
||||||
key={m.id}
|
key={m.id}
|
||||||
contract={m}
|
contract={m}
|
||||||
hideGroupLink
|
hideGroupLink
|
||||||
className="mb-2 max-h-[200px] w-96 shrink-0"
|
className="mb-2 max-h-[200px] w-96 shrink-0 snap-start scroll-m-4 md:snap-align-none"
|
||||||
questionClass="line-clamp-3"
|
questionClass="line-clamp-3"
|
||||||
trackingPostfix=" tournament"
|
trackingPostfix=" tournament"
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user