New fold page UI with tabs (#37)
* Tabbed fold page * Implement market, leaderboard tab views for fold. And edit dialog * Add about field to Fold
This commit is contained in:
		
							parent
							
								
									e481b971d0
								
							
						
					
					
						commit
						8b9d0e5dba
					
				|  | @ -2,6 +2,7 @@ export type Fold = { | |||
|   id: string | ||||
|   slug: string | ||||
|   name: string | ||||
|   about: string | ||||
|   curatorId: string // User id
 | ||||
|   createdTime: number | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall( | |||
|   async ( | ||||
|     data: { | ||||
|       name: string | ||||
|       about: string | ||||
|       tags: string[] | ||||
|     }, | ||||
|     context | ||||
|  | @ -22,7 +23,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall( | |||
|     const creator = await getUser(userId) | ||||
|     if (!creator) return { status: 'error', message: 'User not found' } | ||||
| 
 | ||||
|     const { name, tags } = data | ||||
|     const { name, about, tags } = data | ||||
| 
 | ||||
|     if (!name || typeof name !== 'string') | ||||
|       return { status: 'error', message: 'Name must be a non-empty string' } | ||||
|  | @ -35,7 +36,9 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall( | |||
|       creator.username, | ||||
|       'named', | ||||
|       name, | ||||
|       'on', | ||||
|       'about', | ||||
|       about, | ||||
|       'tags', | ||||
|       tags | ||||
|     ) | ||||
| 
 | ||||
|  | @ -48,6 +51,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall( | |||
|       curatorId: userId, | ||||
|       slug, | ||||
|       name, | ||||
|       about, | ||||
|       tags, | ||||
|       createdTime: Date.now(), | ||||
|       contractIds: [], | ||||
|  |  | |||
							
								
								
									
										128
									
								
								web/components/edit-fold-button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								web/components/edit-fold-button.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,128 @@ | |||
| import { useState } from 'react' | ||||
| import _ from 'lodash' | ||||
| import clsx from 'clsx' | ||||
| import { PencilIcon } from '@heroicons/react/outline' | ||||
| 
 | ||||
| import { Fold } from '../../common/fold' | ||||
| import { parseWordsAsTags } from '../../common/util/parse' | ||||
| import { updateFold } from '../lib/firebase/folds' | ||||
| import { toCamelCase } from '../lib/util/format' | ||||
| import { Spacer } from './layout/spacer' | ||||
| import { TagsList } from './tags-list' | ||||
| 
 | ||||
| export function EditFoldButton(props: { fold: Fold }) { | ||||
|   const { fold } = props | ||||
|   const [name, setName] = useState(fold.name) | ||||
|   const [about, setAbout] = useState(fold.about ?? '') | ||||
| 
 | ||||
|   const initialOtherTags = | ||||
|     fold?.tags.filter((tag) => tag !== toCamelCase(name)).join(', ') ?? '' | ||||
| 
 | ||||
|   const [otherTags, setOtherTags] = useState(initialOtherTags) | ||||
|   const [isSubmitting, setIsSubmitting] = useState(false) | ||||
| 
 | ||||
|   const tags = parseWordsAsTags(toCamelCase(name) + ' ' + otherTags) | ||||
| 
 | ||||
|   const saveDisabled = | ||||
|     name === fold.name && | ||||
|     _.isEqual(tags, fold.tags) && | ||||
|     about === (fold.about ?? '') | ||||
| 
 | ||||
|   const onSubmit = async () => { | ||||
|     setIsSubmitting(true) | ||||
| 
 | ||||
|     await updateFold(fold, { | ||||
|       name, | ||||
|       about, | ||||
|       tags, | ||||
|     }) | ||||
| 
 | ||||
|     setIsSubmitting(false) | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       <label | ||||
|         htmlFor="edit" | ||||
|         className={clsx( | ||||
|           'modal-button text-sm text-gray-700 cursor-pointer whitespace-nowrap' | ||||
|         )} | ||||
|       > | ||||
|         <PencilIcon className="h-4 w-4 inline" /> Edit | ||||
|       </label> | ||||
|       <input type="checkbox" id="edit" className="modal-toggle" /> | ||||
| 
 | ||||
|       <div className="modal"> | ||||
|         <div className="modal-box"> | ||||
|           <div className="form-control w-full"> | ||||
|             <label className="label"> | ||||
|               <span className="mb-1">Fold name</span> | ||||
|             </label> | ||||
| 
 | ||||
|             <input | ||||
|               placeholder="Your fold name" | ||||
|               className="input input-bordered resize-none" | ||||
|               disabled={isSubmitting} | ||||
|               value={name} | ||||
|               onChange={(e) => setName(e.target.value || '')} | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <Spacer h={4} /> | ||||
| 
 | ||||
|           <div className="form-control w-full"> | ||||
|             <label className="label"> | ||||
|               <span className="mb-1">About</span> | ||||
|             </label> | ||||
| 
 | ||||
|             <input | ||||
|               placeholder="Short description (140 characters max)" | ||||
|               className="input input-bordered resize-none" | ||||
|               disabled={isSubmitting} | ||||
|               value={about} | ||||
|               maxLength={140} | ||||
|               onChange={(e) => setAbout(e.target.value || '')} | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <Spacer h={4} /> | ||||
| 
 | ||||
|           <div className="form-control w-full"> | ||||
|             <label className="label"> | ||||
|               <span className="mb-1">Tags</span> | ||||
|             </label> | ||||
| 
 | ||||
|             <input | ||||
|               placeholder="Politics, Economics, Rationality" | ||||
|               className="input input-bordered resize-none" | ||||
|               disabled={isSubmitting} | ||||
|               value={otherTags} | ||||
|               onChange={(e) => setOtherTags(e.target.value || '')} | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <Spacer h={4} /> | ||||
|           <TagsList tags={tags.map((tag) => `#${tag}`)} noLink /> | ||||
|           <Spacer h={4} /> | ||||
| 
 | ||||
|           <div className="modal-action"> | ||||
|             <label htmlFor="edit" className={clsx('btn')}> | ||||
|               Cancel | ||||
|             </label> | ||||
|             <label | ||||
|               className={clsx( | ||||
|                 'btn', | ||||
|                 saveDisabled ? 'btn-disabled' : 'btn-primary', | ||||
|                 isSubmitting && 'loading' | ||||
|               )} | ||||
|               htmlFor="edit" | ||||
|               onClick={onSubmit} | ||||
|             > | ||||
|               Save | ||||
|             </label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  | @ -1,14 +0,0 @@ | |||
| import { ArrowCircleLeftIcon } from '@heroicons/react/outline' | ||||
| import { Fold } from '../../common/fold' | ||||
| import { foldPath } from '../lib/firebase/folds' | ||||
| import { SiteLink } from './site-link' | ||||
| 
 | ||||
| export function FoldBack(props: { fold: Fold }) { | ||||
|   const { fold } = props | ||||
|   return ( | ||||
|     <SiteLink href={foldPath(fold)}> | ||||
|       <ArrowCircleLeftIcon className="h-5 w-5 text-gray-500 inline mr-1" />{' '} | ||||
|       {fold.name} | ||||
|     </SiteLink> | ||||
|   ) | ||||
| } | ||||
|  | @ -1,7 +1,16 @@ | |||
| import clsx from 'clsx' | ||||
| import { CSSProperties } from 'react' | ||||
| 
 | ||||
| export function Col(props: { children?: any; className?: string }) { | ||||
|   const { children, className } = props | ||||
| export function Col(props: { | ||||
|   children?: any | ||||
|   className?: string | ||||
|   style?: CSSProperties | ||||
| }) { | ||||
|   const { children, className, style } = props | ||||
| 
 | ||||
|   return <div className={clsx(className, 'flex flex-col')}>{children}</div> | ||||
|   return ( | ||||
|     <div className={clsx(className, 'flex flex-col')} style={style}> | ||||
|       {children} | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import clsx from 'clsx' | ||||
| import { User } from '../../common/user' | ||||
| import { Row } from './layout/row' | ||||
| import { SiteLink } from './site-link' | ||||
|  | @ -10,10 +11,11 @@ export function Leaderboard(props: { | |||
|     header: string | ||||
|     renderCell: (user: User) => any | ||||
|   }[] | ||||
|   className?: string | ||||
| }) { | ||||
|   const { title, users, columns } = props | ||||
|   const { title, users, columns, className } = props | ||||
|   return ( | ||||
|     <div className="max-w-xl w-full px-1"> | ||||
|     <div className={clsx('w-full px-1', className)}> | ||||
|       <Title text={title} /> | ||||
|       <div className="overflow-x-auto"> | ||||
|         <table className="table table-zebra table-compact text-gray-500 w-full"> | ||||
|  | @ -40,7 +42,7 @@ export function Leaderboard(props: { | |||
|                         width={32} | ||||
|                         height={32} | ||||
|                       /> | ||||
|                       <div>{user.name}</div> | ||||
|                       <div className="truncate">{user.name}</div> | ||||
|                     </Row> | ||||
|                   </SiteLink> | ||||
|                 </td> | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ export const cloudFunction = <RequestData, ResponseData>(name: string) => | |||
| export const createContract = cloudFunction('createContract') | ||||
| 
 | ||||
| export const createFold = cloudFunction< | ||||
|   { name: string; tags: string[] }, | ||||
|   { name: string; about: string; tags: string[] }, | ||||
|   { status: 'error' | 'success'; message?: string; fold?: Fold } | ||||
| >('createFold') | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										312
									
								
								web/pages/fold/[...slugs]/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								web/pages/fold/[...slugs]/index.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,312 @@ | |||
| import _ from 'lodash' | ||||
| import Link from 'next/link' | ||||
| 
 | ||||
| import { Fold } from '../../../../common/fold' | ||||
| import { Comment } from '../../../../common/comment' | ||||
| import { Page } from '../../../components/page' | ||||
| import { Title } from '../../../components/title' | ||||
| import { Bet, listAllBets } from '../../../lib/firebase/bets' | ||||
| import { listAllComments } from '../../../lib/firebase/comments' | ||||
| import { Contract } from '../../../lib/firebase/contracts' | ||||
| import { | ||||
|   foldPath, | ||||
|   getFoldBySlug, | ||||
|   getFoldContracts, | ||||
| } from '../../../lib/firebase/folds' | ||||
| import { ActivityFeed, findActiveContracts } from '../../activity' | ||||
| import { TagsList } from '../../../components/tags-list' | ||||
| import { Row } from '../../../components/layout/row' | ||||
| import { UserLink } from '../../../components/user-page' | ||||
| import { getUser, User } from '../../../lib/firebase/users' | ||||
| import { Spacer } from '../../../components/layout/spacer' | ||||
| import { Col } from '../../../components/layout/col' | ||||
| import { useUser } from '../../../hooks/use-user' | ||||
| import { useFold } from '../../../hooks/use-fold' | ||||
| import { SearchableGrid } from '../../../components/contracts-list' | ||||
| import { useQueryAndSortParams } from '../../../hooks/use-sort-and-query-params' | ||||
| import { useRouter } from 'next/router' | ||||
| import clsx from 'clsx' | ||||
| import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring' | ||||
| import { Leaderboard } from '../../../components/leaderboard' | ||||
| import { formatMoney } from '../../../lib/util/format' | ||||
| import { EditFoldButton } from '../../../components/edit-fold-button' | ||||
| 
 | ||||
| export async function getStaticProps(props: { params: { slugs: string[] } }) { | ||||
|   const { slugs } = props.params | ||||
| 
 | ||||
|   const fold = await getFoldBySlug(slugs[0]) | ||||
|   const curatorPromise = fold ? getUser(fold.curatorId) : null | ||||
| 
 | ||||
|   const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : [] | ||||
|   const contractComments = await Promise.all( | ||||
|     contracts.map((contract) => listAllComments(contract.id).catch((_) => [])) | ||||
|   ) | ||||
| 
 | ||||
|   let activeContracts = findActiveContracts( | ||||
|     contracts, | ||||
|     _.flatten(contractComments), | ||||
|     365 | ||||
|   ) | ||||
|   const [resolved, unresolved] = _.partition( | ||||
|     activeContracts, | ||||
|     ({ isResolved }) => isResolved | ||||
|   ) | ||||
|   activeContracts = [...unresolved, ...resolved] | ||||
| 
 | ||||
|   const activeContractBets = await Promise.all( | ||||
|     activeContracts.map((contract) => listAllBets(contract.id).catch((_) => [])) | ||||
|   ) | ||||
|   const activeContractComments = activeContracts.map( | ||||
|     (contract) => | ||||
|       contractComments[contracts.findIndex((c) => c.id === contract.id)] | ||||
|   ) | ||||
| 
 | ||||
|   const curator = await curatorPromise | ||||
| 
 | ||||
|   const bets = await Promise.all( | ||||
|     contracts.map((contract) => listAllBets(contract.id)) | ||||
|   ) | ||||
| 
 | ||||
|   const creatorScores = scoreCreators(contracts, bets) | ||||
|   const [topCreators, topCreatorScores] = await toUserScores(creatorScores) | ||||
| 
 | ||||
|   const traderScores = scoreTraders(contracts, bets) | ||||
|   const [topTraders, topTraderScores] = await toUserScores(traderScores) | ||||
| 
 | ||||
|   return { | ||||
|     props: { | ||||
|       fold, | ||||
|       curator, | ||||
|       contracts, | ||||
|       activeContracts, | ||||
|       activeContractBets, | ||||
|       activeContractComments, | ||||
|       topTraders, | ||||
|       topTraderScores, | ||||
|       topCreators, | ||||
|       topCreatorScores, | ||||
|     }, | ||||
| 
 | ||||
|     revalidate: 60, // regenerate after a minute
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function toUserScores(userScores: { [userId: string]: number }) { | ||||
|   const topUserPairs = _.take( | ||||
|     _.sortBy(Object.entries(userScores), ([_, score]) => -1 * score), | ||||
|     10 | ||||
|   ) | ||||
|   const topUsers = await Promise.all( | ||||
|     topUserPairs.map(([userId]) => getUser(userId)) | ||||
|   ) | ||||
|   const topUserScores = topUserPairs.map(([_, score]) => score) | ||||
|   return [topUsers, topUserScores] as const | ||||
| } | ||||
| 
 | ||||
| export async function getStaticPaths() { | ||||
|   return { paths: [], fallback: 'blocking' } | ||||
| } | ||||
| 
 | ||||
| export default function FoldPage(props: { | ||||
|   fold: Fold | ||||
|   curator: User | ||||
|   contracts: Contract[] | ||||
|   activeContracts: Contract[] | ||||
|   activeContractBets: Bet[][] | ||||
|   activeContractComments: Comment[][] | ||||
|   topTraders: User[] | ||||
|   topTraderScores: number[] | ||||
|   topCreators: User[] | ||||
|   topCreatorScores: number[] | ||||
|   params: { tab: string } | ||||
| }) { | ||||
|   const { | ||||
|     curator, | ||||
|     contracts, | ||||
|     activeContracts, | ||||
|     activeContractBets, | ||||
|     activeContractComments, | ||||
|     topTraders, | ||||
|     topTraderScores, | ||||
|     topCreators, | ||||
|     topCreatorScores, | ||||
|   } = props | ||||
| 
 | ||||
|   const router = useRouter() | ||||
|   const { slugs } = router.query as { slugs: string[] } | ||||
|   const page = | ||||
|     (slugs[1] as 'markets' | 'leaderboards' | undefined) ?? 'activity' | ||||
| 
 | ||||
|   const fold = useFold(props.fold.id) ?? props.fold | ||||
|   const { curatorId } = fold | ||||
| 
 | ||||
|   const { query, setQuery, sort, setSort } = useQueryAndSortParams({ | ||||
|     defaultSort: 'most-traded', | ||||
|   }) | ||||
| 
 | ||||
|   const user = useUser() | ||||
|   const isCurator = user?.id === curatorId | ||||
| 
 | ||||
|   return ( | ||||
|     <Page wide> | ||||
|       <Col className="items-center"> | ||||
|         <Col className="max-w-5xl w-full"> | ||||
|           <Col className="sm:flex-row sm:justify-between sm:items-end gap-4 mb-6"> | ||||
|             <Title className="!m-0" text={fold.name} /> | ||||
|             {isCurator && <EditFoldButton fold={fold} />} | ||||
|           </Col> | ||||
| 
 | ||||
|           <div className="tabs mb-4"> | ||||
|             <Link href={foldPath(fold)} shallow> | ||||
|               <a | ||||
|                 className={clsx( | ||||
|                   'tab tab-bordered', | ||||
|                   page === 'activity' && 'tab-active' | ||||
|                 )} | ||||
|               > | ||||
|                 Activity | ||||
|               </a> | ||||
|             </Link> | ||||
| 
 | ||||
|             <Link href={foldPath(fold, 'markets')} shallow> | ||||
|               <a | ||||
|                 className={clsx( | ||||
|                   'tab tab-bordered', | ||||
|                   page === 'markets' && 'tab-active' | ||||
|                 )} | ||||
|               > | ||||
|                 Markets | ||||
|               </a> | ||||
|             </Link> | ||||
|             <Link href={foldPath(fold, 'leaderboards')} shallow> | ||||
|               <a | ||||
|                 className={clsx( | ||||
|                   'tab tab-bordered', | ||||
|                   page === 'leaderboards' && 'tab-active', | ||||
|                   page !== 'leaderboards' && 'md:hidden' | ||||
|                 )} | ||||
|               > | ||||
|                 Leaderboards | ||||
|               </a> | ||||
|             </Link> | ||||
|           </div> | ||||
| 
 | ||||
|           {page === 'activity' && ( | ||||
|             <Row className="gap-8"> | ||||
|               <Col> | ||||
|                 <ActivityFeed | ||||
|                   contracts={activeContracts} | ||||
|                   contractBets={activeContractBets} | ||||
|                   contractComments={activeContractComments} | ||||
|                 /> | ||||
|               </Col> | ||||
|               <Col className="hidden md:flex max-w-xs gap-10"> | ||||
|                 <FoldOverview fold={fold} curator={curator} /> | ||||
|                 <FoldLeaderboards | ||||
|                   topTraders={topTraders} | ||||
|                   topTraderScores={topTraderScores} | ||||
|                   topCreators={topCreators} | ||||
|                   topCreatorScores={topCreatorScores} | ||||
|                 /> | ||||
|               </Col> | ||||
|             </Row> | ||||
|           )} | ||||
| 
 | ||||
|           {page === 'markets' && ( | ||||
|             <div className="w-full"> | ||||
|               <SearchableGrid | ||||
|                 contracts={contracts} | ||||
|                 query={query} | ||||
|                 setQuery={setQuery} | ||||
|                 sort={sort} | ||||
|                 setSort={setSort} | ||||
|               /> | ||||
|             </div> | ||||
|           )} | ||||
| 
 | ||||
|           {page === 'leaderboards' && ( | ||||
|             <Col className="gap-8"> | ||||
|               <FoldLeaderboards | ||||
|                 topTraders={topTraders} | ||||
|                 topTraderScores={topTraderScores} | ||||
|                 topCreators={topCreators} | ||||
|                 topCreatorScores={topCreatorScores} | ||||
|               /> | ||||
|             </Col> | ||||
|           )} | ||||
|         </Col> | ||||
|       </Col> | ||||
|     </Page> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| function FoldOverview(props: { fold: Fold; curator: User }) { | ||||
|   const { fold, curator } = props | ||||
|   const { about, tags } = fold | ||||
| 
 | ||||
|   return ( | ||||
|     <Col className="max-w-sm"> | ||||
|       <div className="px-4 py-3 bg-indigo-700 text-white text-sm rounded-t"> | ||||
|         About community | ||||
|       </div> | ||||
|       <Col className="p-4 bg-white self-start gap-2 rounded-b"> | ||||
|         <Row> | ||||
|           <div className="text-gray-500 mr-1">Curated by</div> | ||||
|           <UserLink | ||||
|             className="text-neutral" | ||||
|             name={curator.name} | ||||
|             username={curator.username} | ||||
|           /> | ||||
|         </Row> | ||||
| 
 | ||||
|         {about && ( | ||||
|           <> | ||||
|             <Spacer h={2} /> | ||||
|             <div className="text-gray-500">{about}</div> | ||||
|           </> | ||||
|         )} | ||||
| 
 | ||||
|         <Spacer h={2} /> | ||||
| 
 | ||||
|         <TagsList tags={tags.map((tag) => `#${tag}`)} /> | ||||
|       </Col> | ||||
|     </Col> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| function FoldLeaderboards(props: { | ||||
|   topTraders: User[] | ||||
|   topTraderScores: number[] | ||||
|   topCreators: User[] | ||||
|   topCreatorScores: number[] | ||||
| }) { | ||||
|   const { topTraders, topTraderScores, topCreators, topCreatorScores } = props | ||||
|   return ( | ||||
|     <> | ||||
|       <Leaderboard | ||||
|         className="max-w-xl" | ||||
|         title="🏅 Top traders" | ||||
|         users={topTraders} | ||||
|         columns={[ | ||||
|           { | ||||
|             header: 'Profit', | ||||
|             renderCell: (user) => | ||||
|               formatMoney(topTraderScores[topTraders.indexOf(user)]), | ||||
|           }, | ||||
|         ]} | ||||
|       /> | ||||
|       <Leaderboard | ||||
|         className="max-w-xl" | ||||
|         title="🏅 Top creators" | ||||
|         users={topCreators} | ||||
|         columns={[ | ||||
|           { | ||||
|             header: 'Market pool', | ||||
|             renderCell: (user) => | ||||
|               formatMoney(topCreatorScores[topCreators.indexOf(user)]), | ||||
|           }, | ||||
|         ]} | ||||
|       /> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | @ -1,124 +0,0 @@ | |||
| import clsx from 'clsx' | ||||
| import _ from 'lodash' | ||||
| import { ArrowCircleLeftIcon } from '@heroicons/react/solid' | ||||
| import { useState } from 'react' | ||||
| import { Fold } from '../../../../common/fold' | ||||
| import { parseWordsAsTags } from '../../../../common/util/parse' | ||||
| import { Col } from '../../../components/layout/col' | ||||
| import { Spacer } from '../../../components/layout/spacer' | ||||
| import { Page } from '../../../components/page' | ||||
| import { TagsList } from '../../../components/tags-list' | ||||
| import { | ||||
|   foldPath, | ||||
|   getFoldBySlug, | ||||
|   updateFold, | ||||
| } from '../../../lib/firebase/folds' | ||||
| import Custom404 from '../../404' | ||||
| import { SiteLink } from '../../../components/site-link' | ||||
| import { toCamelCase } from '../../../lib/util/format' | ||||
| import { useFold } from '../../../hooks/use-fold' | ||||
| 
 | ||||
| export async function getStaticProps(props: { params: { foldSlug: string } }) { | ||||
|   const { foldSlug } = props.params | ||||
| 
 | ||||
|   const fold = await getFoldBySlug(foldSlug) | ||||
| 
 | ||||
|   return { | ||||
|     props: { fold }, | ||||
| 
 | ||||
|     revalidate: 60, // regenerate after a minute
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export async function getStaticPaths() { | ||||
|   return { paths: [], fallback: 'blocking' } | ||||
| } | ||||
| 
 | ||||
| export default function EditFoldPage(props: { fold: Fold | null }) { | ||||
|   const fold = useFold(props.fold?.id ?? '') ?? props.fold | ||||
|   const [name, setName] = useState(fold?.name ?? '') | ||||
| 
 | ||||
|   const initialOtherTags = | ||||
|     fold?.tags.filter((tag) => tag !== toCamelCase(name)).join(', ') ?? '' | ||||
| 
 | ||||
|   const [otherTags, setOtherTags] = useState(initialOtherTags) | ||||
|   const [isSubmitting, setIsSubmitting] = useState(false) | ||||
| 
 | ||||
|   if (!fold) return <Custom404 /> | ||||
| 
 | ||||
|   const tags = parseWordsAsTags(toCamelCase(name) + ' ' + otherTags) | ||||
| 
 | ||||
|   const saveDisabled = | ||||
|     !name || (name === fold.name && _.isEqual(tags, fold.tags)) | ||||
| 
 | ||||
|   const onSubmit = async () => { | ||||
|     setIsSubmitting(true) | ||||
| 
 | ||||
|     await updateFold(fold, { | ||||
|       name, | ||||
|       tags, | ||||
|     }) | ||||
| 
 | ||||
|     setIsSubmitting(false) | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Page> | ||||
|       <Col className="items-center"> | ||||
|         <Col className="max-w-2xl w-full px-2 sm:px-0"> | ||||
|           <SiteLink href={foldPath(fold)}> | ||||
|             <ArrowCircleLeftIcon className="h-5 w-5 text-gray-500 inline mr-1" />{' '} | ||||
|             {fold.name} | ||||
|           </SiteLink> | ||||
| 
 | ||||
|           <Spacer h={4} /> | ||||
| 
 | ||||
|           <div className="form-control w-full"> | ||||
|             <label className="label"> | ||||
|               <span className="mb-1">Fold name</span> | ||||
|             </label> | ||||
| 
 | ||||
|             <input | ||||
|               placeholder="Your fold name" | ||||
|               className="input input-bordered resize-none" | ||||
|               disabled={isSubmitting} | ||||
|               value={name} | ||||
|               onChange={(e) => setName(e.target.value || '')} | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <Spacer h={4} /> | ||||
| 
 | ||||
|           <div className="form-control w-full"> | ||||
|             <label className="label"> | ||||
|               <span className="mb-1">Tags</span> | ||||
|             </label> | ||||
| 
 | ||||
|             <input | ||||
|               placeholder="Politics, Economics, Rationality" | ||||
|               className="input input-bordered resize-none" | ||||
|               disabled={isSubmitting} | ||||
|               value={otherTags} | ||||
|               onChange={(e) => setOtherTags(e.target.value || '')} | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <Spacer h={4} /> | ||||
|           <TagsList tags={tags.map((tag) => `#${tag}`)} noLink /> | ||||
|           <Spacer h={4} /> | ||||
| 
 | ||||
|           <button | ||||
|             className={clsx( | ||||
|               'btn self-end', | ||||
|               saveDisabled ? 'btn-disabled' : 'btn-primary', | ||||
|               isSubmitting && 'loading' | ||||
|             )} | ||||
|             onClick={onSubmit} | ||||
|           > | ||||
|             Save | ||||
|           </button> | ||||
|         </Col> | ||||
|       </Col> | ||||
|     </Page> | ||||
|   ) | ||||
| } | ||||
|  | @ -1,142 +0,0 @@ | |||
| import _ from 'lodash' | ||||
| import { Fold } from '../../../../common/fold' | ||||
| import { Comment } from '../../../../common/comment' | ||||
| import { Page } from '../../../components/page' | ||||
| import { Title } from '../../../components/title' | ||||
| import { Bet, listAllBets } from '../../../lib/firebase/bets' | ||||
| import { listAllComments } from '../../../lib/firebase/comments' | ||||
| import { Contract } from '../../../lib/firebase/contracts' | ||||
| import { | ||||
|   foldPath, | ||||
|   getFoldBySlug, | ||||
|   getFoldContracts, | ||||
| } from '../../../lib/firebase/folds' | ||||
| import { ActivityFeed, findActiveContracts } from '../../activity' | ||||
| import { TagsList } from '../../../components/tags-list' | ||||
| import { Row } from '../../../components/layout/row' | ||||
| import { UserLink } from '../../../components/user-page' | ||||
| import { getUser, User } from '../../../lib/firebase/users' | ||||
| import { Spacer } from '../../../components/layout/spacer' | ||||
| import { Col } from '../../../components/layout/col' | ||||
| import { SiteLink } from '../../../components/site-link' | ||||
| import { useUser } from '../../../hooks/use-user' | ||||
| import { useFold } from '../../../hooks/use-fold' | ||||
| 
 | ||||
| export async function getStaticProps(props: { params: { foldSlug: string } }) { | ||||
|   const { foldSlug } = props.params | ||||
| 
 | ||||
|   const fold = await getFoldBySlug(foldSlug) | ||||
|   const curatorPromise = fold ? getUser(fold.curatorId) : null | ||||
| 
 | ||||
|   const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : [] | ||||
|   const contractComments = await Promise.all( | ||||
|     contracts.map((contract) => listAllComments(contract.id).catch((_) => [])) | ||||
|   ) | ||||
| 
 | ||||
|   let activeContracts = findActiveContracts( | ||||
|     contracts, | ||||
|     _.flatten(contractComments), | ||||
|     365 | ||||
|   ) | ||||
|   const [resolved, unresolved] = _.partition( | ||||
|     activeContracts, | ||||
|     ({ isResolved }) => isResolved | ||||
|   ) | ||||
|   activeContracts = [...unresolved, ...resolved] | ||||
| 
 | ||||
|   const activeContractBets = await Promise.all( | ||||
|     activeContracts.map((contract) => listAllBets(contract.id).catch((_) => [])) | ||||
|   ) | ||||
|   const activeContractComments = activeContracts.map( | ||||
|     (contract) => | ||||
|       contractComments[contracts.findIndex((c) => c.id === contract.id)] | ||||
|   ) | ||||
| 
 | ||||
|   const curator = await curatorPromise | ||||
| 
 | ||||
|   return { | ||||
|     props: { | ||||
|       fold, | ||||
|       curator, | ||||
|       activeContracts, | ||||
|       activeContractBets, | ||||
|       activeContractComments, | ||||
|     }, | ||||
| 
 | ||||
|     revalidate: 60, // regenerate after a minute
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export async function getStaticPaths() { | ||||
|   return { paths: [], fallback: 'blocking' } | ||||
| } | ||||
| 
 | ||||
| export default function FoldPage(props: { | ||||
|   fold: Fold | ||||
|   curator: User | ||||
|   activeContracts: Contract[] | ||||
|   activeContractBets: Bet[][] | ||||
|   activeContractComments: Comment[][] | ||||
| }) { | ||||
|   const { | ||||
|     curator, | ||||
|     activeContracts, | ||||
|     activeContractBets, | ||||
|     activeContractComments, | ||||
|   } = props | ||||
| 
 | ||||
|   const fold = useFold(props.fold.id) ?? props.fold | ||||
|   const { tags, curatorId } = fold | ||||
| 
 | ||||
|   const user = useUser() | ||||
|   const isCurator = user?.id === curatorId | ||||
| 
 | ||||
|   return ( | ||||
|     <Page> | ||||
|       <Col className="items-center"> | ||||
|         <Col className="max-w-3xl w-full"> | ||||
|           <Title className="!mt-0" text={fold.name} /> | ||||
| 
 | ||||
|           <Row className="items-center gap-2 mb-2 flex-wrap"> | ||||
|             <SiteLink className="text-sm" href={foldPath(fold, 'markets')}> | ||||
|               Markets | ||||
|             </SiteLink> | ||||
|             <div className="text-gray-500">•</div> | ||||
|             <SiteLink className="text-sm" href={foldPath(fold, 'leaderboards')}> | ||||
|               Leaderboards | ||||
|             </SiteLink> | ||||
|             <div className="text-gray-500">•</div> | ||||
|             <Row> | ||||
|               <div className="text-sm text-gray-500 mr-1">Curated by</div> | ||||
|               <UserLink | ||||
|                 className="text-sm text-neutral" | ||||
|                 name={curator.name} | ||||
|                 username={curator.username} | ||||
|               /> | ||||
|             </Row> | ||||
|             {isCurator && ( | ||||
|               <> | ||||
|                 <div className="text-gray-500">•</div> | ||||
|                 <SiteLink className="text-sm " href={foldPath(fold, 'edit')}> | ||||
|                   Edit | ||||
|                 </SiteLink> | ||||
|               </> | ||||
|             )} | ||||
|           </Row> | ||||
| 
 | ||||
|           <Spacer h={2} /> | ||||
| 
 | ||||
|           <TagsList tags={tags.map((tag) => `#${tag}`)} /> | ||||
| 
 | ||||
|           <Spacer h={8} /> | ||||
| 
 | ||||
|           <ActivityFeed | ||||
|             contracts={activeContracts} | ||||
|             contractBets={activeContractBets} | ||||
|             contractComments={activeContractComments} | ||||
|           /> | ||||
|         </Col> | ||||
|       </Col> | ||||
|     </Page> | ||||
|   ) | ||||
| } | ||||
|  | @ -1,102 +0,0 @@ | |||
| import _ from 'lodash' | ||||
| import { ArrowCircleLeftIcon } from '@heroicons/react/solid' | ||||
| 
 | ||||
| import { Col } from '../../../components/layout/col' | ||||
| import { Leaderboard } from '../../../components/leaderboard' | ||||
| import { Page } from '../../../components/page' | ||||
| import { SiteLink } from '../../../components/site-link' | ||||
| import { formatMoney } from '../../../lib/util/format' | ||||
| import { | ||||
|   foldPath, | ||||
|   getFoldBySlug, | ||||
|   getFoldContracts, | ||||
| } from '../../../lib/firebase/folds' | ||||
| import { Fold } from '../../../../common/fold' | ||||
| import { Spacer } from '../../../components/layout/spacer' | ||||
| import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring' | ||||
| import { getUser, User } from '../../../lib/firebase/users' | ||||
| import { listAllBets } from '../../../lib/firebase/bets' | ||||
| 
 | ||||
| export async function getStaticProps(props: { params: { foldSlug: string } }) { | ||||
|   const { foldSlug } = props.params | ||||
| 
 | ||||
|   const fold = await getFoldBySlug(foldSlug) | ||||
|   const contracts = fold ? await getFoldContracts(fold) : [] | ||||
|   const bets = await Promise.all( | ||||
|     contracts.map((contract) => listAllBets(contract.id)) | ||||
|   ) | ||||
| 
 | ||||
|   const creatorScores = scoreCreators(contracts, bets) | ||||
|   const [topCreators, topCreatorScores] = await toUserScores(creatorScores) | ||||
| 
 | ||||
|   const traderScores = scoreTraders(contracts, bets) | ||||
|   const [topTraders, topTraderScores] = await toUserScores(traderScores) | ||||
| 
 | ||||
|   return { | ||||
|     props: { fold, topTraders, topTraderScores, topCreators, topCreatorScores }, | ||||
| 
 | ||||
|     revalidate: 60, // regenerate after 60 seconds
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export async function getStaticPaths() { | ||||
|   return { paths: [], fallback: 'blocking' } | ||||
| } | ||||
| 
 | ||||
| async function toUserScores(userScores: { [userId: string]: number }) { | ||||
|   const topUserPairs = _.take( | ||||
|     _.sortBy(Object.entries(userScores), ([_, score]) => -1 * score), | ||||
|     10 | ||||
|   ) | ||||
|   const topUsers = await Promise.all( | ||||
|     topUserPairs.map(([userId]) => getUser(userId)) | ||||
|   ) | ||||
|   const topUserScores = topUserPairs.map(([_, score]) => score) | ||||
|   return [topUsers, topUserScores] as const | ||||
| } | ||||
| 
 | ||||
| export default function Leaderboards(props: { | ||||
|   fold: Fold | ||||
|   topTraders: User[] | ||||
|   topTraderScores: number[] | ||||
|   topCreators: User[] | ||||
|   topCreatorScores: number[] | ||||
| }) { | ||||
|   const { fold, topTraders, topTraderScores, topCreators, topCreatorScores } = | ||||
|     props | ||||
|   return ( | ||||
|     <Page> | ||||
|       <SiteLink href={foldPath(fold)}> | ||||
|         <ArrowCircleLeftIcon className="h-5 w-5 text-gray-500 inline mr-1" />{' '} | ||||
|         {fold.name} | ||||
|       </SiteLink> | ||||
| 
 | ||||
|       <Spacer h={4} /> | ||||
| 
 | ||||
|       <Col className="lg:flex-row gap-10"> | ||||
|         <Leaderboard | ||||
|           title="🏅 Top traders" | ||||
|           users={topTraders} | ||||
|           columns={[ | ||||
|             { | ||||
|               header: 'Total profit', | ||||
|               renderCell: (user) => | ||||
|                 formatMoney(topTraderScores[topTraders.indexOf(user)]), | ||||
|             }, | ||||
|           ]} | ||||
|         /> | ||||
|         <Leaderboard | ||||
|           title="🏅 Top creators" | ||||
|           users={topCreators} | ||||
|           columns={[ | ||||
|             { | ||||
|               header: 'Market pool', | ||||
|               renderCell: (user) => | ||||
|                 formatMoney(topCreatorScores[topCreators.indexOf(user)]), | ||||
|             }, | ||||
|           ]} | ||||
|         /> | ||||
|       </Col> | ||||
|     </Page> | ||||
|   ) | ||||
| } | ||||
|  | @ -1,59 +0,0 @@ | |||
| import _ from 'lodash' | ||||
| import { Contract } from '../../../../common/contract' | ||||
| import { Fold } from '../../../../common/fold' | ||||
| import { SearchableGrid } from '../../../components/contracts-list' | ||||
| import { FoldBack } from '../../../components/fold-back' | ||||
| import { Spacer } from '../../../components/layout/spacer' | ||||
| import { Page } from '../../../components/page' | ||||
| import { SEO } from '../../../components/SEO' | ||||
| import { useQueryAndSortParams } from '../../../hooks/use-sort-and-query-params' | ||||
| import { getFoldBySlug, getFoldContracts } from '../../../lib/firebase/folds' | ||||
| 
 | ||||
| export async function getStaticProps(props: { params: { foldSlug: string } }) { | ||||
|   const { foldSlug } = props.params | ||||
| 
 | ||||
|   const fold = await getFoldBySlug(foldSlug) | ||||
|   const contracts = fold ? await getFoldContracts(fold) : [] | ||||
| 
 | ||||
|   return { | ||||
|     props: { | ||||
|       fold, | ||||
|       contracts, | ||||
|     }, | ||||
| 
 | ||||
|     revalidate: 60, // regenerate after a minute
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export async function getStaticPaths() { | ||||
|   return { paths: [], fallback: 'blocking' } | ||||
| } | ||||
| 
 | ||||
| export default function Markets(props: { fold: Fold; contracts: Contract[] }) { | ||||
|   const { fold, contracts } = props | ||||
|   const { query, setQuery, sort, setSort } = useQueryAndSortParams({ | ||||
|     defaultSort: 'most-traded', | ||||
|   }) | ||||
| 
 | ||||
|   return ( | ||||
|     <Page> | ||||
|       <SEO | ||||
|         title={`${fold.name}'s markets`} | ||||
|         description={`Explore or search all the markets of ${fold.name}`} | ||||
|         url="/markets" | ||||
|       /> | ||||
| 
 | ||||
|       <FoldBack fold={fold} /> | ||||
| 
 | ||||
|       <Spacer h={4} /> | ||||
| 
 | ||||
|       <SearchableGrid | ||||
|         contracts={contracts} | ||||
|         query={query} | ||||
|         setQuery={setQuery} | ||||
|         sort={sort} | ||||
|         setSort={setSort} | ||||
|       /> | ||||
|     </Page> | ||||
|   ) | ||||
| } | ||||
|  | @ -103,6 +103,7 @@ export default function Folds(props: { | |||
| 
 | ||||
| function CreateFoldButton() { | ||||
|   const [name, setName] = useState('') | ||||
|   const [about, setAbout] = useState('') | ||||
|   const [otherTags, setOtherTags] = useState('') | ||||
|   const [isSubmitting, setIsSubmitting] = useState(false) | ||||
| 
 | ||||
|  | @ -120,6 +121,7 @@ function CreateFoldButton() { | |||
|     const result = await createFold({ | ||||
|       name, | ||||
|       tags, | ||||
|       about, | ||||
|     }).then((r) => r.data || {}) | ||||
| 
 | ||||
|     if (result.fold) { | ||||
|  | @ -145,7 +147,7 @@ function CreateFoldButton() { | |||
|       }} | ||||
|       submitBtn={{ | ||||
|         label: 'Create', | ||||
|         className: clsx(name ? 'btn-primary' : 'btn-disabled'), | ||||
|         className: clsx(name && about ? 'btn-primary' : 'btn-disabled'), | ||||
|       }} | ||||
|       onSubmit={onSubmit} | ||||
|     > | ||||
|  | @ -175,37 +177,50 @@ function CreateFoldButton() { | |||
| 
 | ||||
|         <Spacer h={4} /> | ||||
| 
 | ||||
|         {name && ( | ||||
|           <> | ||||
|             <label className="label"> | ||||
|               <span className="mb-1">Primary tag</span> | ||||
|             </label> | ||||
|             <TagsList noLink tags={[`#${toCamelCase(name)}`]} /> | ||||
|         <div className="form-control w-full"> | ||||
|           <label className="label"> | ||||
|             <span className="mb-1">About</span> | ||||
|           </label> | ||||
| 
 | ||||
|             <Spacer h={4} /> | ||||
|           <input | ||||
|             placeholder="Short description (140 characters max)" | ||||
|             className="input input-bordered resize-none" | ||||
|             disabled={isSubmitting} | ||||
|             value={about} | ||||
|             maxLength={140} | ||||
|             onChange={(e) => setAbout(e.target.value || '')} | ||||
|           /> | ||||
|         </div> | ||||
| 
 | ||||
|             <div className="form-control w-full"> | ||||
|               <label className="label"> | ||||
|                 <span className="mb-1">Additional tags</span> | ||||
|               </label> | ||||
|         <Spacer h={4} /> | ||||
| 
 | ||||
|               <input | ||||
|                 placeholder="Politics, Economics, Rationality" | ||||
|                 className="input input-bordered resize-none" | ||||
|                 disabled={isSubmitting} | ||||
|                 value={otherTags} | ||||
|                 onChange={(e) => setOtherTags(e.target.value || '')} | ||||
|               /> | ||||
|             </div> | ||||
|         <label className="label"> | ||||
|           <span className="mb-1">Primary tag</span> | ||||
|         </label> | ||||
|         <TagsList noLink tags={[`#${toCamelCase(name)}`]} /> | ||||
| 
 | ||||
|             <Spacer h={4} /> | ||||
|         <Spacer h={4} /> | ||||
| 
 | ||||
|             <TagsList | ||||
|               tags={parseWordsAsTags(otherTags).map((tag) => `#${tag}`)} | ||||
|               noLink | ||||
|             /> | ||||
|           </> | ||||
|         )} | ||||
|         <div className="form-control w-full"> | ||||
|           <label className="label"> | ||||
|             <span className="mb-1">Additional tags</span> | ||||
|           </label> | ||||
| 
 | ||||
|           <input | ||||
|             placeholder="Politics, Economics, Rationality (Optional)" | ||||
|             className="input input-bordered resize-none" | ||||
|             disabled={isSubmitting} | ||||
|             value={otherTags} | ||||
|             onChange={(e) => setOtherTags(e.target.value || '')} | ||||
|           /> | ||||
|         </div> | ||||
| 
 | ||||
|         <Spacer h={4} /> | ||||
| 
 | ||||
|         <TagsList | ||||
|           tags={parseWordsAsTags(otherTags).map((tag) => `#${tag}`)} | ||||
|           noLink | ||||
|         /> | ||||
|       </div> | ||||
|     </ConfirmationButton> | ||||
|   ) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user