diff --git a/common/user.ts b/common/user.ts index 0e333278..f15865cf 100644 --- a/common/user.ts +++ b/common/user.ts @@ -34,7 +34,7 @@ export type User = { followerCountCached: number followedCategories?: string[] - homeSections?: { visible: string[]; hidden: string[] } + homeSections?: string[] referredByUserId?: string referredByContractId?: string diff --git a/web/components/arrange-home.tsx b/web/components/arrange-home.tsx index ae02e3ea..25e814b8 100644 --- a/web/components/arrange-home.tsx +++ b/web/components/arrange-home.tsx @@ -13,19 +13,13 @@ import { Group } from 'common/group' export function ArrangeHome(props: { user: User | null | undefined - homeSections: { visible: string[]; hidden: string[] } - setHomeSections: (homeSections: { - visible: string[] - hidden: string[] - }) => void + homeSections: string[] + setHomeSections: (sections: string[]) => void }) { const { user, homeSections, setHomeSections } = props const groups = useMemberGroups(user?.id) ?? [] - const { itemsById, visibleItems, hiddenItems } = getHomeItems( - groups, - homeSections - ) + const { itemsById, sections } = getHomeItems(groups, homeSections) return ( <DragDropContext @@ -35,23 +29,16 @@ export function ArrangeHome(props: { const item = itemsById[draggableId] - const newHomeSections = { - visible: visibleItems.map((item) => item.id), - hidden: hiddenItems.map((item) => item.id), - } + const newHomeSections = sections.map((section) => section.id) - const sourceSection = source.droppableId as 'visible' | 'hidden' - newHomeSections[sourceSection].splice(source.index, 1) - - const destSection = destination.droppableId as 'visible' | 'hidden' - newHomeSections[destSection].splice(destination.index, 0, item.id) + newHomeSections.splice(source.index, 1) + newHomeSections.splice(destination.index, 0, item.id) setHomeSections(newHomeSections) }} > - <Row className="relative max-w-lg gap-4"> - <DraggableList items={visibleItems} title="Visible" /> - <DraggableList items={hiddenItems} title="Hidden" /> + <Row className="relative max-w-md gap-4"> + <DraggableList items={sections} title="Sections" /> </Row> </DragDropContext> ) @@ -64,16 +51,13 @@ function DraggableList(props: { const { title, items } = props return ( <Droppable droppableId={title.toLowerCase()}> - {(provided, snapshot) => ( + {(provided) => ( <Col {...provided.droppableProps} ref={provided.innerRef} - className={clsx( - 'width-[220px] flex-1 items-start rounded bg-gray-50 p-2', - snapshot.isDraggingOver && 'bg-gray-100' - )} + className={clsx('flex-1 items-stretch gap-1 rounded bg-gray-100 p-4')} > - <Subtitle text={title} className="mx-2 !my-2" /> + <Subtitle text={title} className="mx-2 !mt-0 !mb-4" /> {items.map((item, index) => ( <Draggable key={item.id} draggableId={item.id} index={index}> {(provided, snapshot) => ( @@ -82,16 +66,13 @@ function DraggableList(props: { {...provided.draggableProps} {...provided.dragHandleProps} style={provided.draggableProps.style} - className={clsx( - 'flex flex-row items-center gap-4 rounded bg-gray-50 p-2', - snapshot.isDragging && 'z-[9000] bg-gray-300' - )} > - <MenuIcon - className="h-5 w-5 flex-shrink-0 text-gray-500" - aria-hidden="true" - />{' '} - {item.label} + <SectionItem + className={clsx( + snapshot.isDragging && 'z-[9000] bg-gray-200' + )} + item={item} + /> </div> )} </Draggable> @@ -103,15 +84,33 @@ function DraggableList(props: { ) } -export const getHomeItems = ( - groups: Group[], - homeSections: { visible: string[]; hidden: string[] } -) => { +const SectionItem = (props: { + item: { id: string; label: string } + className?: string +}) => { + const { item, className } = props + + return ( + <div + className={clsx( + className, + 'flex flex-row items-center gap-4 rounded bg-gray-50 p-2' + )} + > + <MenuIcon + className="h-5 w-5 flex-shrink-0 text-gray-500" + aria-hidden="true" + />{' '} + {item.label} + </div> + ) +} + +export const getHomeItems = (groups: Group[], sections: string[]) => { const items = [ + { label: 'Daily movers', id: 'daily-movers' }, { label: 'Trending', id: 'score' }, - { label: 'Newest', id: 'newest' }, - { label: 'Close date', id: 'close-date' }, - { label: 'Your trades', id: 'your-bets' }, + { label: 'New for you', id: 'newest' }, ...groups.map((g) => ({ label: g.name, id: g.id, @@ -119,23 +118,13 @@ export const getHomeItems = ( ] const itemsById = keyBy(items, 'id') - const { visible, hidden } = homeSections + const sectionItems = filterDefined(sections.map((id) => itemsById[id])) - const [visibleItems, hiddenItems] = [ - filterDefined(visible.map((id) => itemsById[id])), - filterDefined(hidden.map((id) => itemsById[id])), - ] - - // Add unmentioned items to the visible list. - visibleItems.push( - ...items.filter( - (item) => !visibleItems.includes(item) && !hiddenItems.includes(item) - ) - ) + // Add unmentioned items to the end. + sectionItems.push(...items.filter((item) => !sectionItems.includes(item))) return { - visibleItems, - hiddenItems, + sections: sectionItems, itemsById, } } diff --git a/web/components/contract/prob-change-table.tsx b/web/components/contract/prob-change-table.tsx index f973d260..49216b88 100644 --- a/web/components/contract/prob-change-table.tsx +++ b/web/components/contract/prob-change-table.tsx @@ -2,74 +2,69 @@ import clsx from 'clsx' import { contractPath } from 'web/lib/firebase/contracts' import { CPMMContract } from 'common/contract' import { formatPercent } from 'common/util/format' -import { useProbChanges } from 'web/hooks/use-prob-changes' -import { linkClass, SiteLink } from '../site-link' +import { SiteLink } from '../site-link' import { Col } from '../layout/col' import { Row } from '../layout/row' -import { useState } from 'react' +import { LoadingIndicator } from '../loading-indicator' -export function ProbChangeTable(props: { userId: string | undefined }) { - const { userId } = props +export function ProbChangeTable(props: { + changes: + | { positiveChanges: CPMMContract[]; negativeChanges: CPMMContract[] } + | undefined +}) { + const { changes } = props - const changes = useProbChanges(userId ?? '') - const [expanded, setExpanded] = useState(false) - - if (!changes) { - return null - } - - const count = expanded ? 16 : 4 + if (!changes) return <LoadingIndicator /> const { positiveChanges, negativeChanges } = changes - const filteredPositiveChanges = positiveChanges.slice(0, count / 2) - const filteredNegativeChanges = negativeChanges.slice(0, count / 2) - const filteredChanges = [ - ...filteredPositiveChanges, - ...filteredNegativeChanges, - ] + + const threshold = 0.075 + const countOverThreshold = Math.max( + positiveChanges.findIndex((c) => c.probChanges.day < threshold) + 1, + negativeChanges.findIndex((c) => c.probChanges.day > -threshold) + 1 + ) + const maxRows = Math.min(positiveChanges.length, negativeChanges.length) + const rows = Math.min(3, Math.min(maxRows, countOverThreshold)) + + const filteredPositiveChanges = positiveChanges.slice(0, rows) + const filteredNegativeChanges = negativeChanges.slice(0, rows) + + if (rows === 0) return <div className="px-4 text-gray-500">None</div> return ( - <Col> - <Col className="mb-4 w-full divide-x-2 divide-y rounded-lg bg-white shadow-md md:flex-row md:divide-y-0"> - <Col className="flex-1 divide-y"> - {filteredChanges.slice(0, count / 2).map((contract) => ( - <Row className="items-center hover:bg-gray-100"> - <ProbChange - className="p-4 text-right text-xl" - contract={contract} - /> - <SiteLink - className="p-4 pl-2 font-semibold text-indigo-700" - href={contractPath(contract)} - > - <span className="line-clamp-2">{contract.question}</span> - </SiteLink> - </Row> - ))} - </Col> - <Col className="flex-1 divide-y"> - {filteredChanges.slice(count / 2).map((contract) => ( - <Row className="items-center hover:bg-gray-100"> - <ProbChange - className="p-4 text-right text-xl" - contract={contract} - /> - <SiteLink - className="p-4 pl-2 font-semibold text-indigo-700" - href={contractPath(contract)} - > - <span className="line-clamp-2">{contract.question}</span> - </SiteLink> - </Row> - ))} - </Col> + <Col className="mb-4 w-full divide-x-2 divide-y rounded-lg bg-white shadow-md md:flex-row md:divide-y-0"> + <Col className="flex-1 divide-y"> + {filteredPositiveChanges.map((contract) => ( + <Row className="items-center hover:bg-gray-100"> + <ProbChange + className="p-4 text-right text-xl" + contract={contract} + /> + <SiteLink + className="p-4 pl-2 font-semibold text-indigo-700" + href={contractPath(contract)} + > + <span className="line-clamp-2">{contract.question}</span> + </SiteLink> + </Row> + ))} + </Col> + <Col className="flex-1 divide-y"> + {filteredNegativeChanges.map((contract) => ( + <Row className="items-center hover:bg-gray-100"> + <ProbChange + className="p-4 text-right text-xl" + contract={contract} + /> + <SiteLink + className="p-4 pl-2 font-semibold text-indigo-700" + href={contractPath(contract)} + > + <span className="line-clamp-2">{contract.question}</span> + </SiteLink> + </Row> + ))} </Col> - <div - className={clsx(linkClass, 'cursor-pointer self-end')} - onClick={() => setExpanded(!expanded)} - > - {expanded ? 'Show less' : 'Show more'} - </div> </Col> ) } diff --git a/web/pages/experimental/home/edit.tsx b/web/pages/experimental/home/edit.tsx index 2cba3f19..2ed9d2dd 100644 --- a/web/pages/experimental/home/edit.tsx +++ b/web/pages/experimental/home/edit.tsx @@ -16,14 +16,9 @@ export default function Home() { useTracking('edit home') - const [homeSections, setHomeSections] = useState( - user?.homeSections ?? { visible: [], hidden: [] } - ) + const [homeSections, setHomeSections] = useState(user?.homeSections ?? []) - const updateHomeSections = (newHomeSections: { - visible: string[] - hidden: string[] - }) => { + const updateHomeSections = (newHomeSections: string[]) => { if (!user) return updateUser(user.id, { homeSections: newHomeSections }) setHomeSections(newHomeSections) @@ -31,7 +26,7 @@ export default function Home() { return ( <Page> - <Col className="pm:mx-10 gap-4 px-4 pb-12"> + <Col className="pm:mx-10 gap-4 px-4 pb-6 pt-2"> <Row className={'w-full items-center justify-between'}> <Title text="Edit your home page" /> <DoneButton /> diff --git a/web/pages/experimental/home/index.tsx b/web/pages/experimental/home/index.tsx index 90b4f888..08f502b6 100644 --- a/web/pages/experimental/home/index.tsx +++ b/web/pages/experimental/home/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React from 'react' import Router from 'next/router' import { PencilIcon, @@ -28,6 +28,7 @@ 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' +import { useProbChanges } from 'web/hooks/use-prob-changes' const Home = () => { const user = useUser() @@ -38,10 +39,7 @@ const Home = () => { const groups = useMemberGroups(user?.id) ?? [] - const [homeSections] = useState( - user?.homeSections ?? { visible: [], hidden: [] } - ) - const { visibleItems } = getHomeItems(groups, homeSections) + const { sections } = getHomeItems(groups, user?.homeSections ?? []) return ( <Page> @@ -54,29 +52,19 @@ const Home = () => { <DailyProfitAndBalance userId={user?.id} /> - <div className="text-xl text-gray-800">Daily movers</div> - <ProbChangeTable userId={user?.id} /> - - {visibleItems.map((item) => { + {sections.map((item) => { const { id } = item - if (id === 'your-bets') { - return ( - <SearchSection - key={id} - label={'Your trades'} - sort={'newest'} - user={user} - yourBets - /> - ) + if (id === 'daily-movers') { + return <DailyMoversSection key={id} userId={user?.id} /> } const sort = SORTS.find((sort) => sort.value === id) if (sort) return ( <SearchSection key={id} - label={sort.label} + label={sort.value === 'newest' ? 'New for you' : sort.label} sort={sort.value} + followed={sort.value === 'newest'} user={user} /> ) @@ -103,11 +91,12 @@ const Home = () => { function SearchSection(props: { label: string - user: User | null | undefined + user: User | null | undefined | undefined sort: Sort yourBets?: boolean + followed?: boolean }) { - const { label, user, sort, yourBets } = props + const { label, user, sort, yourBets, followed } = props const href = `/home?s=${sort}` return ( @@ -122,7 +111,13 @@ function SearchSection(props: { <ContractSearch user={user} defaultSort={sort} - additionalFilter={yourBets ? { yourBets: true } : { followed: true }} + additionalFilter={ + yourBets + ? { yourBets: true } + : followed + ? { followed: true } + : undefined + } noControls maxResults={6} persistPrefix={`experimental-home-${sort}`} @@ -131,7 +126,10 @@ function SearchSection(props: { ) } -function GroupSection(props: { group: Group; user: User | null | undefined }) { +function GroupSection(props: { + group: Group + user: User | null | undefined | undefined +}) { const { group, user } = props return ( @@ -155,6 +153,24 @@ function GroupSection(props: { group: Group; user: User | null | undefined }) { ) } +function DailyMoversSection(props: { userId: string | null | undefined }) { + const { userId } = props + const changes = useProbChanges(userId ?? '') + + return ( + <Col className="gap-2"> + <SiteLink className="text-xl" href={'/daily-movers'}> + Daily movers{' '} + <ArrowSmRightIcon + className="mb-0.5 inline h-6 w-6 text-gray-500" + aria-hidden="true" + /> + </SiteLink> + <ProbChangeTable changes={changes} /> + </Col> + ) +} + function EditButton(props: { className?: string }) { const { className } = props @@ -186,14 +202,14 @@ function DailyProfitAndBalance(props: { return ( <div className={clsx(className, 'text-lg')}> <span className={clsx(profit >= 0 ? 'text-green-500' : 'text-red-500')}> - {profit >= 0 ? '+' : '-'} + {profit >= 0 && '+'} {formatMoney(profit)} </span>{' '} profit and{' '} <span className={clsx(balanceChange >= 0 ? 'text-green-500' : 'text-red-500')} > - {balanceChange >= 0 ? '+' : '-'} + {balanceChange >= 0 && '+'} {formatMoney(balanceChange)} </span>{' '} balance today